Содержание
В C# 5.0 добавлены ключевые слова await
и async
для поддержки асинхронного программирования — стиля программирования, когда длительные по времени выполнения функции проделывают всю или большую часть своей работы после возврата контроля в точку вызова. В противоположность этому при обычном — синхронном — программировании длительные функции блокируют вызвавший их код пока не завершат свое выполнение. Асинхронное программирование предполагает параллелизм (параллельное выполнение нескольких операций), когда длительные операции выполняются параллельно с вызвавшим их кодом. Параллельное выполнение асинхронных функций реализуется либо через многопоточность (для операций, зависящих только от быстродействия процессора — compute-bound), либо через механизм обратного вызова (для операций, ограниченных скоростью ввода/вывода данных — I/O-bound).
Например, следующий метод выполняется синхронно, является длительным по времени выполнения и зависит от быстродействия процессора (compute-bound):
1 2 3 4 5 6 7 | int ComplexCalculation() { double x = 2; for (int i = 1; i < 100000000; i++) x += Math.Sqrt (x) / i; return (int)x; } |
Этот метод блокирует вызвавший его код на несколько секунд пока продолжается его выполнение. Затем результат вычисления возвращается в точку вызова:
1 2 3 | int result = ComplexCalculation(); // После задержки: Console.WriteLine (result); // 116 |
В CLR предусмотрен класс Task<TResult>
(в System.Threading.Tasks
) для реализации концепции операций, завершающихся в будущем. Для операций, зависящих от быстродействия процессора, можно создать экземпляр Task<TResult>
вызвав Task.Run
и передав ему в качестве параметра делегат. CLR запустит этот делегат в отдельном потоке параллельно с вызвавшим кодом:
1 2 3 4 | Task<int> ComplexCalculationAsync() { return Task.Run (() => ComplexCalculation()); } |
Этот метод асинхронный, так как возвращает контроль над выполнением в точку вызова сразу же пока сам выполняется параллельно. Однако необходим специальный механизм, позволяющий вызвавшему коду определить, что должно случиться когда операция завершиться и результат станет доступным. Класс Task<TResult>
реализует это с помощью метода GetAwaiter
, который позволяет добавить в точку вызова отложенное продолжение:
1 2 3 4 5 6 7 | Task<int> task = ComplexCalculationAsync(); var awaiter = task.GetAwaiter(); awaiter.OnCompleted (() => // Продолжение { int result = awaiter.GetResult(); Console.WriteLine (result); // 116 }); |
Отложенное продолжение представляет собой делегат, который будет вызван после того как асинхронный метод будет завершен. Чтобы получить результат выполнения асинхронного метода внутри делегата необходимо вызвать метод GetResult
.
Ключевые слова await и async
Ключевое слово await
упрощает синтаксис добавления отложенного продолжения:
1 2 3 4 5 6 7 8 9 | var result = await expression; statement(s); // Эквивалентно: var awaiter = expression.GetAwaiter(); awaiter.OnCompleted (() => { var result = awaiter.GetResult(); statement(s); ); |
Соответственно пример из предыдущего раздела можно переписать так:
1 2 | int result = await ComplexCalculationAsync(); Console.WriteLine (result); |
Также необходимо добавить родительскому методу модификатор async
:
1 2 3 4 5 | async void Test() { int result = await ComplexCalculationAsync(); Console.WriteLine (result); } |
Модификатор 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
, который не возвращает никакого значения:
1 2 | await Task.Delay (5000); Console.WriteLine ("Five seconds passed!"); |
Task.Delay
— статический метод, возвращающий Task
, который завершиться по истечении указанного количества миллисекунд. Его синхронный эквивалент — Thread.Sleep
.
Сам по себе Task
— не обобщенный базовый класс для Task<TResult>
. Их функциональность эквивалента, за исключением того, что Task
не возвращает результат.
Захват локального состояния
Выражения с ключевым словом await
могут использоваться в любом месте кода (в пределах асинхронных функций), за исключением блоков catch
и finally
, выражений с ключевым словом lock
, контекста ключевого слова unsafe
и точки входа (метод main
).
Например, его можно использовать внутри цикла:
1 2 3 4 5 6 7 8 | async void Test() { for (int i = 0; i < 10; i++) { int result = await ComplexCalculationAsync(); Console.WriteLine (result); } } |
После завершения выполнения отложенного метода ComplexCalculationAsync
выполнение возвращается из точки вызова в асинхронную функцию Test
в то место, на котором оно прервалось. При этом сохраняется значение всех локальных переменных и счетчиков цикла. Это достигается благодаря тому, что компилятор автоматически создает конечный автомат (state machine). Без использования ключевого слова await
конечный автомат пришлось бы реализовывать самостоятельно, что традиционно затрудняло асинхронное программирование.
Возврат значения асинхронной функцией
Асинхронной функции в качестве возвращаемого типа можно указать не только void
, но и Task
. В этом случает сама асинхронная функция становится ожидаемой (т.е. ее вызов может использоваться также как выражение с ключевым словом await
):
1 2 3 4 5 6 | async Task PrintAnswerToLife() { await Task.Delay (5000); int answer = 21 * 2; Console.WriteLine (answer); } |
При этом явно объект Task
из функции не возвращается, компилятор сам создаст объект Task
после того как метод будет завершен. Это делает возможным создавать цепочки асинхронных вызовов:
1 2 3 4 5 | async Task Go() { await PrintAnswerToLife(); Console.WriteLine ("Done"); } |
При этом все точки вызова в цепочке будут автоматически получать объект Task
по завершении выполнения ожидаемой функции (асинхронной функции возвращаемой Task
).
Также асинхронной функции в качестве возвращаемого типа можно указать Task<TResult>
. При этом в теле метода должно возвращаться значение типа TResult
:
1 2 3 4 5 6 | async Task<int> GetAnswerToLife() { await Task.Delay (5000); int answer = 21 * 2; return answer; } |
Возвращаемый результат может быть использован в точке вызова:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | async Task Go() { await PrintAnswerToLife(); Console.WriteLine ("Done"); } async Task PrintAnswerToLife() { int answer = await GetAnswerToLife(); Console.WriteLine (answer); } async Task<int> GetAnswerToLife() { await Task.Delay (5000); int answer = 21 * 2; return answer; } |
Асинхронные функции делают асинхронное программирование схожим с синхронным. Синхронный вариант примера выше буде выглядеть практически также (и даст тот же результат):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void Go() { PrintAnswerToLife(); Console.WriteLine ("Done"); } void PrintAnswerToLife() { int answer = GetAnswerToLife(); Console.WriteLine (answer); } int GetAnswerToLife() { Thread.Sleep (5000); int answer = 21 * 2; return answer; } |
Это иллюстрирует главный принцип асинхронного программирования в C#: писать методы синхронными, а затем заменить синхронный вызов методов на асинхронный и ждать (await) их.
Параллельность
Выше был рассмотрен самый распространенный шаблон: вызов функции возвращающей объект Task и сразу после вызова ожидание ее завершения. Однако такой подход схож с синхронным программированием.
Вызов асинхронного метода без ожидания его завершения позволяет коду, идущему после вызова этого метода выполняться параллельно. В следующем примере функция PrintAnswerToLife
выполняется дважды и параллельно:
1 2 3 | var task1 = PrintAnswerToLife(); var task2 = PrintAnswerToLife(); await task1; await task2; |
Ожидание обеих операций в конце прекращает параллельное выполнение в этой точке. Класс Task
содержит статический метод WhenAll
, позволяющий достичь того же результата немного более эффективным способом. Метод вернет объект Task
, когда все переданные ему асинхронные методы будут завершены:
1 2 | await Task.WhenAll (PrintAnswerToLife(), PrintAnswerToLife()); |
Класс Task
также содержит статический метод WhenAny
, который завершится при завершении любого из переданных ему методов.
Асинхронные лямбда выражения
Точно также как обычные именованные методы могут быть асинхронными:
1 2 3 4 5 | async Task NamedMethod() { await Task.Delay (1000); Console.WriteLine ("Foo"); } |
1 2 3 4 5 | Func<Task> unnamed = async () => { await Task.Delay (1000); Console.WriteLine ("Foo"); }; |
1 2 | await NamedMethod(); await unnamed(); |
1 2 3 4 5 | myButton.Click += async (sender, args) => { await Task.Delay (1000); myButton.Content = "Done"; }; |
1 2 3 4 5 6 | myButton.Click += ButtonHandler; async void ButtonHander (object sender, EventArgs args) { await Task.Delay (1000); myButton.Content = "Done"; }; |
1 2 3 4 5 6 | Func<Task<int>> unnamed = async () => { await Task.Delay (1000); return 123; }; int answer = await unnamed(); |