Объяснение await, async C#
Как правило, асинхронность подразумевает выполнение операции в стиле, не подразумевающем блокирование вызвавшего потока, то есть запуск операции без ожидания ее завершения.
Приготовление завтрака — хороший пример асинхронной и непараллельной работы. Один человек (или поток) может справиться со всеми этими задачами. Продолжая аналогию с завтраком, один человек может приготовить завтрак асинхронно, начав следующую задачу до завершения первой. Приготовление продолжается независимо от того, наблюдает ли за ним кто-то или нет. Как только вы начнете разогревать сковороду для яиц, можно приступать к жарке бекона. Как только бекон начнет поджариваться, можно положить хлеб в тостер.
Для параллельного алгоритма вам понадобится несколько поваров (или потоков). Кто-то готовил яйца, кто-то бекон и так далее. Каждый из них будет сосредоточен только на одной задаче. Каждый повар (или поток) будет синхронно заблокирован в ожидании, пока бекон будет готов перевернуться или тост лопнет.
Стандартный асинхронный метод в стиле TAP написать очень просто.
- Чтобы возвращаемое значение было Task, Task<T> или void. В C# 7 пришли Task-like типы. В C# 8 к этому списку добавляется еще IAsyncEnumerable<T> и IAsyncEnumerator<T>
- Чтобы метод был помечен ключевым словом async, а внутри содержал await. Эти ключевые слова идут в паре. При этом если метод содержит await, обязательно его помечать async, обратное неверно, но бессмысленно
- Для приличия соблюдать конвенцию о суффиксе Async.
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static void Main(string[] args)
{
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");
Bacon bacon = FryBacon(3);
Console.WriteLine("bacon is ready");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
watch.Stop();
Console.WriteLine(quot;Execution Time: { watch.ElapsedMilliseconds}ms");
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static Bacon FryBacon(int slices)
{
Console.WriteLine(quot;putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
Task.Delay(3000).Wait();
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine(quot;cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
} Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}Синхронно приготовленный завтрак занял примерно 30 минут, что соответствует 15 секундам работы программы
Сделаем main асинхронным и добавим асинхронные методы FryEggsAsync, FryBaconAsync, ToastBreadAsync
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = await FryEggsAsync(2);
Console.WriteLine("eggs are ready");
Bacon bacon = await FryBaconAsync(3);
Console.WriteLine("bacon is ready");
Toast toast = await ToastBreadAsync(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
watch.Stop();
Console.WriteLine(quot;Execution Time: {watch.ElapsedMilliseconds}ms");
}
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine(quot;putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine(quot;cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static Bacon FryBacon(int slices)
{
Console.WriteLine(quot;putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
Task.Delay(3000).Wait();
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine(quot;cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}Проверим, ускорилась ли программа:
Как видно из вывода, наша программа нисколько не ускорилась, и работает почти также как и синхронная. Так в чем же дело? Мы же добавили там, где нужно ключевые слова await. Все дело в том, что асинхронное выполнение программы стартует при вызове метода асинхронного метода var eggs = FryEggsAsync(2);
Слово await не позволяет выполняться коду, находящемуся ниже await до тех пор, пока асинхронный метод не закончит свою работу. Это своеобразная контрольная точка.
Ключевое слово await не позволяет вызвавшему асинхронный метод потоку блокироваться при длительной работе этого метода. Напротив, мы возвращаем этот поток в пул потоков и этот поток может быть использован для других операций. Если по окончании ожидания(по завершению работы асинхронной операции), вызывающий поток будет занят, то мы возьмем другой поток из пула потоков и вызовем продолжение выполнения метода синхронно(или асинхронно если там дальше есть еще асинхронные методы).
Если ждать завершение асинхронного метода не обязательно(нам не важен результат работы метода), то ключевое слово await можно не ставить.
То есть, если же мы сразу запустим с await var eggs = await FryEggsAsync(2), то такой запуск будет равносилен синхронному выполнению кода. Мы запускаем асинхронный метод и словом await запрещаем выполнение дальнейшего кода, пока этот метод не завершит свою работу. Так что же тогда делать? Использовать await там, где уже будет невозможно дальнейшее продолжения программы без получения результата асинхронного метода. В нашем коде методы ApplyButter(toast) и ApplyJam(toast) ждут приготовленного тоста, поэтому логичнее всего использовать await перед вызовом этих методов. Для eggs и bacon строгих ограничений нет, поэтому их awaitы можно поместить в самый конец перед тем как распечатать, что завтрак готов.
static async Task Main(string[] args)
{
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");
Console.WriteLine("Breakfast is ready!");
watch.Stop();
Console.WriteLine(quot;Execution Time: {watch.ElapsedMilliseconds}ms");}onds}ms");
}Как видим, время выполнения завтрака уменьшилось с 15 до 6 секунд. На этот раз экономия времени обусловлена тем, что некоторые задачи выполнялись одновременно. Тост, яичница и бекон начали выполнение одновременно. Так как тост готовится 3 секунды, а два других метода 6, то суммарное время и получилось 6 секунд.
Вы ожидаете выполнения каждой задачи только тогда, когда вам нужны результаты.
Композиция асинхронной операции, за которой следует синхронная работа, является асинхронной операцией. Другими словами, если какая-либо часть операции является асинхронной, то вся операция является асинхронной.
Прежде чем подавать завтрак, вам нужно дождаться поджаривания хлеба, прежде чем добавлять масло и джем. Вы можете представить эту работу с помощью следующего кода:
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);return toast; }
Теперь основным блоком кода станет:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var eggs = await eggsTask;
Console.WriteLine("eggs are ready");
var bacon = await baconTask;
Console.WriteLine("bacon is ready");
var toast = await toastTask;
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}Предыдущее изменение иллюстрировало важную технику работы с асинхронным кодом. Вы составляете задачи, разделяя операции на новый метод, который возвращает задачу. Вы можете выбрать, когда ожидать выполнения этой задачи. Вы можете одновременно запускать другие задачи.
Ряд awaitоператоров в конце предыдущего кода можно улучшить, используя методы класса Task. Одним из таких API является WhenAll , который возвращает Task , который завершается, когда все задачи в его списке аргументов завершены, как показано в следующем коде:
await Task.WhenAll(eggsTask, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");Другой вариант — использовать WhenAny , который возвращает a Task<Task>, который завершается, когда завершается любой из его аргументов.
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("Eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("Bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("Toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}После всех этих изменений окончательная версия кода выглядит так:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
watch.Stop();
Console.WriteLine(quot;Execution Time: {watch.ElapsedMilliseconds}ms");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine(quot;putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine(quot;cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}Данная версия асинхронно приготовленного завтрака заняла также 6 секунд. То есть 6 секунд это минимальное время, необходимое для приготовления завтрака.
Выполнение метода с точки зрения поведения происходит так.
- Выполняется весь код, предшествующий вызову асинхронной операции.
- Выполняется вызов асинхронной операции. На данном этапе поток не освобождается и не блокируется. Данная операция возвращает результат — упомянутый объект задачи (как правило Task), который сохраняется в локальную переменную
- Выполняется код после вызова асинхронной операции, но до ожидания (await).
- Ожидание завершения на объекте задачи (который сохранили в локальную переменную) — await task.
- Если асинхронная операция к этому моменту завершена, то выполнение продолжается синхронно, в том же потоке.
- Если асинхронная операция не завершена, то сохраняется код, который надо будет вызвать по завершении асинхронной операции, а поток возвращается в пул потоков и становится доступен для использования.
- Выполнение операций после ожидания — выполняется или сразу же, в том же потоке, когда операция на момент ожидания была завершена, или, по завершении операции, берется новый поток, который выполнит продолжение (сохраненное на предыдущем шаге)
Преобразования компилятора
public void CopyStreamToStream(Stream source, Stream destination)
{
var buffer = new byte[0x1000];
int numRead;
while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
{
destination.Write(buffer, 0, numRead);
}
}вот как выглядит соответствующий метод с async/await:
public async Task CopyStreamToStreamAsync(Stream source, Stream destination)
{
var buffer = new byte[0x1000];
int numRead;
while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
await destination.WriteAsync(buffer, 0, numRead);
}
}Как и в случае с итераторами, компилятор переписывает асинхронный метод в метод, основанный на машине состояний.
[AsyncStateMachine(typeof(<CopyStreamToStreamAsync>d__0))]
public Task CopyStreamToStreamAsync(Stream source, Stream destination)
{//Компилятор сгенерировал структуру
<CopyStreamToStreamAsync>d__0 stateMachine = default;
//Эта структура является машиной состояния для метода
//После инициализации машины состояний мы видим вызов
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.source = source;
stateMachine.destination = destination;
stateMachine.<>1__state = -1;
// используется для выполнения начального MoveNext на машине состояний
stateMachine.<>t__builder.Start(ref stateMachine);
// возвращает результат выполнения задачи
return stateMachine.<>t__builder.Task;
}
// Машина состояний - структура, в которую преобразуются Task-методы.
private struct <CopyStreamToStreamAsync>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
public Stream source;
public Stream destination;
private byte[] <buffer>5__2;
private TaskAwaiter <>u__1;
private TaskAwaiter<int> <>u__2;
...
}Компилятор сгенерировал структуру <CopyStreamToStreamAsync>d__0, и он инициализировал экземпляр этой структуры на стеке. Важно отметить, что если метод async завершится синхронно, то эта машина состояния никогда не покинет стек. Это означает, что с машиной состояний не связано никаких выделений памяти, если только метод не должен завершиться асинхронно, то есть он await чего-то, что еще не завершено к этому моменту.
Эта структура является машиной состояния для метода, содержащей не только всю преобразованную логику из того, что написал разработчик, но и поля для отслеживания текущей позиции в этом методе, а также все «локальное» состояние, которое компилятор извлек из метода и которое должно сохраниться между вызовами MoveNext
После инициализации машины состояний мы видим вызов AsyncTaskMethodBuilder.Create(). Хотя мы сейчас сосредоточены на Task, язык C# и компилятор позволяют возвращать произвольные типы («task-like» типы) из async методов, например, я могу написать метод public async MyTask CopyStreamToStreamAsync, и он будет прекрасно компилироваться, если мы дополним MyTask, который мы определили ранее, соответствующим образом. Этот подходящий способ включает объявление связанного типа «builder» и ассоциирование его с типом через атрибут AsyncMethodBuilder:
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder))]
public class MyTask
{
...
}
public struct MyTaskMethodBuilder
{
public static MyTaskMethodBuilder Create() { ... }
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { ... }
public void SetStateMachine(IAsyncStateMachine stateMachine) { ... }
public void SetResult() { ... }
public void SetException(Exception exception) { ... }
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine { ... }
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine { ... }
public MyTask Task { get { ... } }
}В данном контексте такой «builder» - это нечто, что умеет создавать экземпляр данного типа (свойство Task), завершать его либо успешно и с результатом, если это необходимо (SetResult), либо с исключением (SetException), а также обрабатывать подключение продолжений к ожиданию того, что еще не завершилось (AwaitOnCompleted/AwaitUnsafeOnCompleted). В случае System.Threading.Tasks.Task он по умолчанию ассоциирован с AsyncTaskMethodBuilder.
В результате компилятор нашел конструктор, который можно использовать для этого асинхронного метода, и конструирует его экземпляр с помощью метода Create, который является частью паттерна.
Затем машина состояний заполняется аргументами этого метода точки входа. Эти параметры должны быть доступны в теле метода, который был перемещен в MoveNext, и поэтому эти аргументы должны храниться в машине состояний, чтобы на них мог ссылаться код при последующем вызове MoveNext. Машина состояний также инициализируется, чтобы находиться в начальном состоянии -1. Если вызвать MoveNext, а состояние будет равно -1, то логически мы начнем с начала метода.
Теперь самая неприметная, но самая важная строка: вызов метода Start конструктора. Это еще одна часть паттерна, которая должна быть раскрыта на типе, используемом в позиции возврата асинхронного метода, и она используется для выполнения начального MoveNext на машине состояний.
Вызов stateMachine.<>t__builder.Start(ref stateMachine); на самом деле просто вызывает stateMachine.MoveNext().
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
stateMachine.MoveNext();
}ExecutionContext
Тип ExecutionContext - это средство, с помощью которого окружающие данные переходят от асинхронной операции к асинхронной операции. Он живет в [ThreadStatic], но затем, когда инициируется какая-то асинхронная операция, он «захватывается» (причудливый способ сказать «прочитать копию из этого статического потока»), сохраняется, а затем, когда выполняется продолжение этой асинхронной операции, ExecutionContext сначала восстанавливается, чтобы жить в [ThreadStatic] на потоке, который собирается выполнить операцию. ExecutionContext - это механизм, с помощью которого реализуется AsyncLocal<T> (фактически, в .NET Core ExecutionContext - это полностью AsyncLocal<T>, не более того), так что если вы храните значение в AsyncLocal<T>, а затем, например, ставите в очередь рабочий элемент для выполнения на ThreadPool, это значение будет видно в AsyncLocal<T> внутри рабочего элемента, выполняющегося на пуле:
var number = new AsyncLocal<int>(); number.Value = 42; ThreadPool.QueueUserWorkItem(_ => Console.WriteLine(number.Value)); number.Value = 0; Console.ReadLine();
Это будет выводить 42 при каждом запуске. Не имеет значения, что в момент после постановки делегата в очередь мы сбросили значение AsyncLocal<int> обратно в 0, потому что ExecutionContext был захвачен как часть вызова QueueUserWorkItem, и этот захват включал состояние AsyncLocal<int> в тот самый момент.
Поэтому реализация AsyncTaskMethodBuilder.Start выглядит следующим образом:
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
ExecutionContext previous = Thread.CurrentThread._executionContext; // [ThreadStatic] field
try
{
stateMachine.MoveNext();
}
finally
{
ExecutionContext.Restore(previous); // internal helper
}
}Вместо того, чтобы просто вызвать stateMachine.MoveNext() мы выполняем процедуру получения текущего ExecutionContext, затем вызываем MoveNext, а после его завершения сбрасываем текущий контекст обратно в тот, который был до вызова MoveNext.
Это делается для того, чтобы предотвратить утечку данных из асинхронного метода в вызывающий.
MoveNext
Итак, был вызван метод точки входа, инициализирована структура машины состояний, вызван Start, который вызвал MoveNext. Что такое MoveNext? Это метод, который содержит всю оригинальную логику метода разработчика, но с целым рядом изменений. Давайте начнем с того, что посмотрим на структуру метода. Вот декомпилированная версия того, что выдает компилятор для нашего метода, но с удалением всего внутри сгенерированного блока try:
private void MoveNext()
{
try
{
... // all of the code from the CopyStreamToStreamAsync method body, but not exactly as it was written
}
catch (Exception exception)
{
<>1__state = -2;
<buffer>5__2 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<buffer>5__2 = null;
<>t__builder.SetResult();
}Какую бы другую работу ни выполнял MoveNext, он несет ответственность за завершение задачи, возвращаемой из метода async Task, когда вся работа будет выполнена. Если тело блока try выбросит исключение, которое не будет обработано, то задача будет завершена с этим исключением. Если же метод async успешно достигнет своего конца (что эквивалентно возвращению синхронного метода), то он успешно завершит возвращенную задачу. В любом из этих случаев он устанавливает состояние машины состояний, указывающее на завершение.
Tакже обратите внимание, что это завершение проходит через билдер, используя его методы SetException и SetResult, которые являются частью шаблона для конструктора, ожидаемого компилятором. Если асинхронный метод ранее приостанавливался, то билдер уже должен был создать задачу в рамках обработки приостановки, и в этом случае вызов SetException/SetResult завершит Task. Если же асинхронный метод ранее не приостанавливался, то мы еще не создали Task и не вернули ничего вызывающему, поэтому у конструктора есть больше гибкости в том, как он создаст эту Task.
Билдер знает, если метод когда-либо приостанавливался, в этом случае у него есть уже созданная Task, и он просто возвращает ее. Если метод никогда не приостанавливался и у билдера еще нет задачи, он может создать здесь завершенную задачу. В этом случае, при успешном завершении, он может просто использовать Task.CompletedTask вместо выделения новой задачи, избегая выделения памяти. В случае общего Task<TResult> билдер может просто использовать Task.FromResult<TResult>(TResult result).
Task фактически имеет три возможных конечных состояния: успех, неудача и отмена.
Теперь, когда мы понимаем аспекты жизненного цикла, вот все, что заполняется внутри блока try в MoveNext:
private void MoveNext()
{
try
{
int num = <>1__state;
TaskAwaiter<int> awaiter;
if (num != 0)
{
if (num != 1)
{
<buffer>5__2 = new byte[4096];
goto IL_008b;
}
awaiter = <>u__2;
<>u__2 = default(TaskAwaiter<int>);
num = (<>1__state = -1);
goto IL_00f0;
}
TaskAwaiter awaiter2 = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
IL_0084:
awaiter2.GetResult();
IL_008b:
awaiter = source.ReadAsync(<buffer>5__2, 0, <buffer>5__2.Length).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 1);
<>u__2 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
IL_00f0:
int result;
if ((result = awaiter.GetResult()) != 0)
{
awaiter2 = destination.WriteAsync(<buffer>5__2, 0, result).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter2;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);
return;
}
goto IL_0084;
}
}
catch (Exception exception)
{
<>1__state = -2;
<buffer>5__2 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<buffer>5__2 = null;
<>t__builder.SetResult();
}Подобные сложности могут показаться немного знакомыми. Помните, каким запутанным был наш реализованный вручную BeginCopyStreamToStream на основе APM? Это не так сложно, но и гораздо лучше, поскольку компилятор делает работу за нас, переписывая метод в форме передачи продолжений и гарантируя, что все необходимое состояние сохраняется для этих продолжений. Несмотря на это, мы можем присмотреться и проследить за развитием событий. Помните, что в точке входа состояние было инициализировано на -1. Затем мы входим в MoveNext, обнаруживаем, что это состояние (которое теперь хранится в num локально) не равно ни 0, ни 1, и таким образом выполняем код, который создает временный буфер и затем переходит на метку IL_008b, где выполняет вызов stream.ReadAsync. Обратите внимание, что в этот момент мы все еще выполняемся синхронно от вызова MoveNext, а значит синхронно от Start, а значит синхронно от точки входа, что означает, что код разработчика вызвал CopyStreamToStreamAsync и он все еще синхронно выполняется, еще не вернув обратно Task, чтобы представить окончательное завершение этого метода. Возможно, это скоро изменится...
Мы вызываем Stream.ReadAsync и получаем от него Task<int>. Чтение могло завершиться синхронно, могло завершиться асинхронно, но так быстро, что теперь оно уже завершено, или оно могло еще не завершиться. В любом случае, у нас есть Task<int>, который представляет его возможное завершение, и компилятор выдает код, который проверяет этот Task<int>, чтобы определить, как действовать дальше: если Task<int> действительно уже завершен (не имеет значения, был ли он завершен синхронно или просто к моменту проверки), то код для этого метода может просто продолжать работать синхронно... нет смысла тратить лишние накладные расходы на постановку в очередь рабочего элемента для обработки оставшейся части выполнения метода, когда вместо этого мы можем просто продолжать работать здесь и сейчас. Но для обработки случая, когда Task<int> не завершился, компилятору необходимо выдать код для подключения продолжения к Task. Таким образом, он должен выдать код, который спрашивает Task «ты закончил?». Обращается ли он непосредственно к Task, чтобы спросить об этом?
Было бы ограничением, если бы единственной вещью, которую можно было бы ожидать в C#, была System.Threading.Tasks.Task. Аналогично, было бы ограничением, если бы компилятор C# должен был знать о каждом возможном типе, который может быть ожидаемым. Вместо этого C# поступает так, как обычно поступает в подобных случаях: он использует шаблон API. Код может ожидать все, что раскрывает соответствующий шаблон, шаблон «ожидающий» (точно так же, как вы можете выполнять foreach везде, где используется соответствующий шаблон «перечислимый»).
Например, мы можем дополнить тип MyTask, который мы написали ранее, для реализации паттерна ожидания:
The State Machine
Чтобы успешно перемещаться по различным этапам асинхронного выполнения, компилятор генерирует конечный автомат, который упаковывает наш асинхронный код. Cуществует четыре состояния.
- Не начался
- Выполнение
- Приостановлено
- Завершено (это состояние включает как успешное, так и ошибочное завершение)
Компилятор, похоже, не особо заботится о точном значении, которое представляет каждое из этих состояний. Это означает, что состояния «не запущено» и «выполнение» обозначаются -1, а «завершенное» (успешное или ошибочное) обозначается -2. Любое другое значение состояния считается «приостановленным» (скорее всего, в выражении await).
private sealed class DoSomethingAsync_Compiler : IAsyncStateMachine
{
private void MoveNext()
{
// ... implementation covered in next section :)
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}MoveNext() — это «Где происходит волшебство»
Другая (и потенциально более важная) реализация метода, о которой мы поговорим дальше, — это MoveNext()метод.
private sealed class DoSomethingAsync_Compiler : IAsyncStateMachine
{
public int state; // Represents the current state of the state machine
public AsyncTaskMethodBuilder builder; // Key to starting the state machine & propagating the result
public int input; // Input to the method
private TaskAwaiter awaiter1; // Important for keeping track of the awaiter on continuations
// Method implementations from last snippet...
}Теперь мы готовы рассмотреть и обсудить MoveNext()назначение и реализацию метода. Первое, что следует отметить в отношении этого MoveNext()метода, это то, что он вызывается при запуске конечного автомата и снова каждый раз, когда конечный автомат возобновляет работу после паузы (скорее всего, в выражении await). Начнем с того, что конечный автомат запускается посредством вызова AsyncTaskMethodBuilder.Start(), который я отметил в комментарии к builderполю в предыдущем фрагменте. Этот Start()метод изначально вызывает MoveNext(). Теперь давайте посмотрим на сгенерированный компилятором код для этого MoveNextметода.
private void MoveNext()
{
int num = state;
try
{
TaskAwaiter awaiter;
if (num != 0) // 1
{
Console.WriteLine("Before awaiting.");
awaiter = Task.Delay(input).GetAwaiter();
if (!awaiter.IsCompleted) // 2
{
num = (state = 0);
awaiter1 = awaiter;
DoSomethingAsync_Compiler stateMachine = this;
builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); // 3
return;
}
}
else
{ // 4
awaiter = awaiter1;
awaiter1 = default(TaskAwaiter);
num = (state = -1); // 5
}
awaiter.GetResult(); // 6
Console.WriteLine("Task has been awaited.");
}
catch (Exception exception)
{
state = -2;
builder.SetException(exception); // 7
return;
}
state = -2;
builder.SetResult(); // 8
}Разбивка MoveNext()
- Первоначальная проверка состояния (
numвсе время синхронизируется с состоянием) - От того, будет ли выполнена Задача, зависит, как будет действовать конечный автомат.
- Планирует конечный автомат перейти к следующему действию после завершения указанного ожидания.
- Возобновление с продолжения
- Установите состояние, указывающее, что задача запущена и/или выполняется.
- Получите результат
awaiter, который сделает его доступным для компоновщика, поскольку он был переданAwaitUnsafeOnCompletedпо ссылке. - Произошло исключение, и неисправная задача распространяется через
SetException()метод, а не выбрасывается. - Метод завершается, и Задача возвращается со статусом завершения. Если тип возвращаемого значения равен
Task<TResult>, результат также устанавливается.
Основной конечный автомат метода работает.void MoveNext();Интересная часть заключается в том, что он ничего не возвращает, поскольку изменяет состояние по ссылке. По своей конструкции он полагается на завершение ожидания, и то, что он на самом деле делает, хорошо описано в документации:Moves the state machine to its next state.
StateMachineсам может иметь три разных состояния для отслеживания статуса задачи:
enum State
{
Completed = -2,
Created = -1,
Awaiting = 0,
}После AsyncTaskMethodBuilderнажатия команды Start метод конечного автоматаMoveNextвыполняется.
Что MoveNextделает управление состоянием: оно проверяет, завершено ли ожидание или нет, и изменяет состояние конечного автомата и/или задачи. Как только конечный автомат наконец-то сможет пометить задачу как выполненную, он вызывает ее,Builder.SetResult();и все готово.
GetValueFromCacheAsyncсейчас инициализирует конечный автомат (2) и впервые запускает обработку/планирование задач (3) .
Как только начинается обработка задачи, среда выполнения проверяет, может ли задача быть выполнена синхронно, и если да, то вызывается Task.ExecuteEntry, задача помечается как выполненная, и мы сразу же получаем выполнение задачи с результатом.
Если задача не может выполняться синхронно, по умолчанию в игру вступает организация очереди ThreadPool.
Организация очереди ThreadPool работает с событиями и обратными вызовами, а также с асинхронными операциями. Существует два типа обработки: с привязкой к ЦП и с привязкой к вводу-выводу. При действительно асинхронных операциях, таких как запись на диск или выполнение сетевых запросов, .NET ожидает уведомления о завершении собственного процесса и назначает поток для продолжения обработки результата встроенного сервиса. Это то, что называется операциями, связанными с вводом-выводом. Для привязки к процессору может существовать поток для обработки результата в фоновом режиме. Для лучшего понимания того, как это возможно, рекомендую прочитать статью Стивена Клири — там нет ветки .
Поэтому, как только ThreadPool уведомляется о завершении асинхронной операции, он снова уведомляет Async State MachineMoveNext() и выполняется во второй раз.
MoveNextпроверяет, завершено ли ожидание и, поскольку это так - устанавливает результат задачи и она выполнена. Конечный автомат завершает работу, и .NET продолжает обработку.
Асинхронные задачи в .NET выполняют почти то же самое, что мы обсуждали в предыдущей главе, но заключены в красивый небольшой пакет с богатой экосистемой взаимодействующих классов. Компилятор C# преобразует все «асинхронные» методы в классы, производные от IAsyncStateMachine . Все локальные переменные становятся членами класса, каждый фрагмент кода между двумя последовательными инструкциями «await» помещается в блок if-else или switch-case, идентифицируемый номером от 0 и выше. Это число становится «состоянием» конечного автомата. Каждый раз, когда блок кода заканчивается, он создает TaskAwaiter.и возвращает его вызывающей стороне, чтобы узнать, когда можно вызвать следующий блок кода (или следующее состояние). Все исключения перехватываются и сохраняются в членах класса, чтобы к ним можно было обратиться в более поздний момент времени, а не в потоке, в котором они фактически произошли.