День двести восемнадцатый. #ЗаметкиНаПолях

Многопоточность.

9. Async/await. Продолжение

Асинхронные анонимные функции

Асинхронные анонимные функции создаются, как и любой другой анонимный метод или лямбда-выражение, просто добавлением модификатора async в начале:

Func<Task> lambda = async () => await Task.Delay(1000);

Func<Task<int>> anonMethod = async delegate()

{

Console.WriteLine("Started");

await Task.Delay(1000);

Console.WriteLine("Finished");

return 10;

};

Делегат должен иметь сигнатуру с типом возврата, подходящим для асинхронного метода (void, Task, Task <TResult> в C#5 и 6 или пользовательским типом задачи в C#7). Он может захватывать переменные, как и другие анонимные функции, и иметь параметры. Кроме того, асинхронная операция не запускается до тех пор, пока не будет вызван делегат, а несколько вызовов делегата создают несколько операций.



Пользовательские типы заданий

В C#5 и 6 асинхронные функции могут возвращать только void, Task или Task <TResult>. C#7 слегка ослабляет это ограничение и позволяет любому типу, оформленному особым образом, использоваться в качестве возвращаемого типа для асинхронных функций.

Тип System.Threading.ValueTask<TResult> присутствует в стандартной комплектации только в инфраструктуре netcoreapp2.0, но он также доступен в NuGet пакете System.Threading.Tasks.Extensions. Он используется в 99,9% случаев. Хотя, можно создать собственный пользовательский тип задания.

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

В чем преимущество ValueTask<TResult>? Всё сводится к выделению памяти в куче и сборке мусора. Task<TResult> является классом, и хотя в некоторых случаях асинхронная инфраструктура повторно использует завершенные объекты Task<TResult>, большинству асинхронных методов потребуется создавать новый объект Task<TResult>. Размещение объектов в .NET достаточно дёшево, поэтому во многих случаях вам не нужно об этом беспокоиться, но если вы делаете это много раз или работаете в условиях жестких ограничений производительности, вы можете избежать такого размещения, когда это возможно. Если асинхронный метод использует выражение await для чего-то незавершённого, выделение объектов неизбежно. Метод немедленно возвращается, но должен запланировать продолжение для выполнения оставшейся части метода после завершения ожидаемой операции.

В большинстве асинхронных методов это наиболее вероятный случай. В этих случаях ValueTask<TResult> не даёт никаких преимуществ и может даже быть немного дороже.

Однако в некоторых случаях, ожидание уже завершенной задачи является наиболее вероятным исходом. И именно здесь полезен ValueTask<TResult>. Например, чтение с использованием буфера (когда размер буфера много больше размера разового чтения). В редких случаях, когда буфер пустой, мы ожидаем завершения чтения в буфер. В остальных случаях (чаще всего) ожидания не требуется, асинхронный метод выполняется без продолжения, и тогда ValueTask<TResult> выигрывает.



Продолжение следует…



Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.