Появление async/await в C# привело к пересмотру того, как писать простой и корректный параллельный код. Зачастую, используя асинхронное программирование, программисты не только не решают проблемы, которые были с потоками, но и привносят новые. Дедлоки и рейсы никуда не пропадают — их просто становится труднее диагностировать.
Дмитрий Иванов — Software Analysis TeamLead в Huawei, в прошлом техлид JetBrains Rider и разработчик ядра ReSharper: структур данных, кэшей, многопоточности, регулярный спикер конференции DotNext.
Далее повествование от лица спикера.
В многопоточном или асинхронном коде часто что-то ломается. Причиной может быть как deadlock, так и race. Как правило, race падает один раз из тысячи, зачастую не локально, а только на билд-сервере, и нужно несколько дней, чтобы его поймать. Уверен, для многих это знакомая ситуация.
Кроме того, просматривая асинхронный код даже опытных разработчиков, я ловлю себя на мысли, что некоторые вещи можно записать в три раза короче и правильнее.
Это наводит на мысль, что проблема не в людях, а в инструменте. Люди просто используют инструмент и хотят, чтобы он решал их задачу. Сам инструмент обладает очень большим количеством возможностей (иногда даже лишних), настроек, неявным контекстом, что приводит к тому, что его очень легко использовать неправильно. Давайте попробуем разобраться, как правильно использовать async/await и работать с классом Task в .NET.
Зачем вообще нужны async/await? Допустим, у нас есть код, работающий с общей разделяемой памятью.
В начале работы мы считываем запрос, в данном случае — файл из блокирующей очереди (например, из интернета или с диска), c помощью блокирующего запроса Dequeue (блокирующие запросы будут помечены красным на картинках с примерами).
Этот подход требует много потоков, а каждый поток требует ресурсов, создает нагрузку на scheduler. Но это не основная проблема. Предположим, люди смогли бы переписать операционные системы так, чтобы эти системы поддерживали и сто тысяч, и миллион потоков. Но основная проблема в том, что некоторые потоки просто нельзя занимать. Например, у вас есть поток пользовательского интерфейса. Нормальных адекватных UI-фреймворков, где доступ к данным был бы не только с одного потока, пока нет. UI-поток нельзя блокировать. И чтобы его не блокировать нам потребуется асинхронный код.