Speak.Me Учить иностранные слова

C#: асинхронные функции

В C# 5.0 добавлены ключевые слова await и async для поддержки асинхронного программирования — стиля программирования, когда длительные по времени выполнения функции проделывают всю или большую часть своей работы после возврата контроля в точку вызова. В противоположность этому при обычном — синхронном — программировании длительные функции блокируют вызвавший их код пока не завершат свое выполнение. Асинхронное программирование предполагает параллелизм (параллельное выполнение нескольких операций), когда длительные операции выполняются параллельно с вызвавшим их кодом. Параллельное выполнение асинхронных функций реализуется либо через многопоточность (для операций, зависящих только от быстродействия процессора — compute-bound), либо через механизм обратного вызова (для операций, ограниченных скоростью ввода/вывода данных — I/O-bound).

Например, следующий метод выполняется синхронно, является длительным по времени выполнения и зависит от быстродействия процессора (compute-bound):

Этот метод блокирует вызвавший его код на несколько секунд пока продолжается его выполнение. Затем результат вычисления возвращается в точку вызова:

В CLR предусмотрен класс Task<TResult>System.Threading.Tasks) для реализации концепции операций, завершающихся в будущем. Для операций, зависящих от быстродействия процессора, можно создать экземпляр Task<TResult> вызвав Task.Run и передав ему в качестве параметра делегат. CLR запустит этот делегат в отдельном потоке параллельно с вызвавшим кодом:

Этот метод асинхронный, так как возвращает контроль над выполнением в точку вызова сразу же пока сам выполняется параллельно. Однако необходим специальный механизм, позволяющий вызвавшему коду определить, что должно случиться когда операция завершиться и результат станет доступным. Класс Task<TResult> реализует это с помощью метода GetAwaiter, который позволяет добавить в точку вызова отложенное продолжение:

Отложенное продолжение представляет собой делегат, который будет вызван после того как асинхронный метод будет завершен. Чтобы получить результат выполнения асинхронного метода внутри делегата необходимо вызвать метод GetResult.

Ключевые слова await и async

Ключевое слово await упрощает синтаксис добавления отложенного продолжения:

Соответственно пример из предыдущего раздела можно переписать так:

Также необходимо добавить родительскому методу модификатор async:

Модификатор async заставляет компилятор воспринимать await как ключевое слово, а не как идентификатор типа (это сделано с цель обратной совместимости с кодом, написанным до появления C# 5.0). Модификатор async может быть применен только к методу (или лямбда выражению) возвращаемому void либо Task или Task<TResult>. Модификатор async (также как модификатор unsafe) не является частью сигнатуры метода или его метаданных.

Методы с модификатором async называются асинхронными функциями, поскольку в действительности выполняются асинхронно. Встретив выражение с ключевым словом await, выполнение возвращается в точку вызова (также как при встрече yield return в итераторе). Но перед возвращением создается отложенное продолжение для ожидания завершения задания. Когда отложенное задание завершиться, выполнение вернется назад в метод и продолжит его выполнение с того момента, на котором оно остановилось. Если отложенное задание завершиться с ошибкой, будет выброшено исключение, в противном случае возвращенное значение будет считаться результатом выполнения выражения с ключевым словом await.

В пределах асинхронной функции компилятор ожидает встретить либо выражение с ключевым слово await, которое и рассматривается как отложенное задание, либо любой объект с методом GetAwaiter, возвращающим отложенный объект (awaitable object) — объект, реализующий интерфейс INotifyCompletion.OnCompleted и содержащий метод GetResult (возвращающий соответствующий результат), а также свойство bool IsCompleted, проверяющее завершенность синхронного выполнения.

Помимо обобщенного класса Task<TResult> можно использовать и не обобщенный класс Task, который не возвращает никакого значения:

Task.Delay — статический метод, возвращающий Task, который завершиться по истечении указанного количества миллисекунд. Его синхронный эквивалент — Thread.Sleep.

Сам по себе Task — не обобщенный базовый класс для Task<TResult>. Их функциональность эквивалента, за исключением того, что Task не возвращает результат.

Захват локального состояния

Выражения с ключевым словом await могут использоваться в любом месте кода (в пределах асинхронных функций), за исключением блоков catch и finally, выражений с ключевым словом lock, контекста ключевого слова unsafe и точки входа (метод main).

Например, его можно использовать внутри цикла:

После завершения выполнения отложенного метода ComplexCalculationAsync выполнение возвращается из точки вызова в асинхронную функцию Test в то место, на котором оно прервалось. При этом сохраняется значение всех локальных переменных и счетчиков цикла. Это достигается благодаря тому, что компилятор автоматически создает конечный автомат (state machine). Без использования ключевого слова await конечный автомат пришлось бы реализовывать самостоятельно, что традиционно затрудняло асинхронное программирование.

Возврат значения асинхронной функцией

Асинхронной функции в качестве возвращаемого типа можно указать не только void, но и Task. В этом случает сама асинхронная функция становится ожидаемой (т.е. ее вызов может использоваться также как выражение с ключевым словом await):

При этом явно объект Task из функции не возвращается, компилятор сам создаст объект Task после того как метод будет завершен. Это делает возможным создавать цепочки асинхронных вызовов:

При этом все точки вызова в цепочке будут автоматически получать объект Task по завершении выполнения ожидаемой функции (асинхронной функции возвращаемой Task).

Также асинхронной функции в качестве возвращаемого типа можно указать Task<TResult>. При этом в теле метода должно возвращаться значение типа TResult:

Возвращаемый результат может быть использован в точке вызова:

Асинхронные функции делают асинхронное программирование схожим с синхронным. Синхронный вариант примера выше буде выглядеть практически также (и даст тот же результат):

Это иллюстрирует главный принцип асинхронного программирования в C#: писать методы синхронными, а затем заменить синхронный вызов методов на асинхронный и ждать (await) их.

Параллельность

Выше был рассмотрен самый распространенный шаблон: вызов функции возвращающей объект Task и сразу после вызова ожидание ее завершения. Однако такой подход схож с синхронным программированием.

Вызов асинхронного метода без ожидания его завершения позволяет коду, идущему после вызова этого метода выполняться параллельно. В следующем примере функция PrintAnswerToLife выполняется дважды и параллельно:

Ожидание обеих операций в конце прекращает параллельное выполнение в этой точке. Класс Task содержит статический метод WhenAll, позволяющий достичь того же результата немного более эффективным способом. Метод вернет объект Task, когда все переданные ему асинхронные методы будут завершены:

Класс Task также содержит статический метод WhenAny, который завершится при завершении любого из переданных ему методов.

Асинхронные лямбда выражения

Точно также как обычные именованные методы могут быть асинхронными:

безымянные методы: лямбда выражения и анонимные методы — тоже могут быть асинхронными, если перед ними поставить ключевое слово async:
В последствии эти методы можно вызывать и ожидать как обычные:
Асинхронные лямбда выражения могут использоваться при добавлении обработчиков событий:
Это более лаконично чем следующий код, дающий такой же результат:
Асинхронные лямбда выражения также могут возвращать Task<TResult>: