Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
Знакомство с ключевыми словами async и await языка C# (обновление в версиях 7.1, 9.0)
Ключевое слово async языка C# применяется для указания на то, что метод, лямбда-выражение или анонимный метод должен вызываться в асинхронной манере автоматически. Да, это правда. Благодаря простой пометке метода модификатором async среда .NET Core Runtime будет создавать новый поток выполнения для обработки текущей задачи. Более того, при вызове метода async ключевое слово await будет автоматически приостанавливать текущий поток до тех пор, пока задача не завершится, давая возможность вызывающему потоку продолжить свою работу.
В целях иллюстрации создайте новый проект консольного приложения по имени FunWithCSharpAsync и импортируйте в файл Program.cs пространства имен System.Threading, System.Threading.Task и System.Collections.Generic. Добавьте метод DoWork(), который заставляет вызывающий поток ожидать пять секунд. Ниже показан код:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
Console.WriteLine(" Fun With Async ===>");
Console.WriteLine(DoWork());
Console.WriteLine("Completed");
Console.ReadLine();
static string DoWork()
{
Thread.Sleep(5_000);
return "Done with work!";
}
Вам известно, что после запуска программы придется ожидать пять секунд, прежде чем сможет произойти что-то еще. В случае графического приложения весь пользовательский интерфейс был бы заблокирован до тех пор, пока работа не завершится.
Если бы мы решили прибегнуть к одному из описанных ранее приемов, чтобы сделать приложение более отзывчивым, тогда пришлось бы немало потрудиться. Тем не менее, начиная с версии .NET 4.5, можно написать следующий код С#:
...
string message = await DoWorkAsync();
Console.WriteLine(message);
...
static string DoWork()
{
Thread.Sleep(5_000);
return "Done with work!";
}
static async Task<string> DoWorkAsync()
{
return await Task.Run(() =>
{
Thread.Sleep(5_000);
return "Done with work!";
});
}
Если вы используете в качестве точки входа метод Main() (вместо операторов верхнего уровня), тогда должны пометить метод с помощью ключевого слова async, появившегося в версии C# 7.1:
static async Task Main(string[] args)
{
...
string message = await DoWorkAsync();
Conole.WriteLine(message);
...
}
На заметку! Возможность декорирования метода Main() посредством async — нововведение, появившееся в версии C# 7.1. Операторы верхнего уровня в версии C# 9.0 являются неявно асинхронными.
Обратите внимание на ключевое слово await перед именем метода, который будет вызван в неблокирующей манере. Это важно: если метод декорируется ключевым словом async, но не имеет хотя бы одного внутреннего вызова метода с использованием await, то получится синхронный вызов (на самом деле компилятор выдаст соответствующее предупреждение).
Кроме того, вы должны применять класс Task из пространства имен System.Threading.Tasks для переделки методов Main() (если вы используете Main()) и DoWork() (последний добавляется как DoWorkAsync()). По существу вместо возвращения просто специфического значения (объекта string в текущем примере) возвращается объект Task<T>, где обобщенный параметр типа Т представляет собой действительное возвращаемое значение.
Реализация метода DoWorkAsync() теперь напрямую возвращает объект Task<T>, который является возвращаемым значением Task.Run(). Метод Run() принимает делегат Func<> или Action<> и, как вам уже известно, для простоты здесь можно использовать лямбда-выражение. В целом новая версия DoWorkAsync() может быть описана следующим образом.
При вызове запускается новая задача, которая заставляет вызывающий поток уснуть на пять секунд. После завершения вызывающий поток предоставляет строковое возвращаемое значение. Эта строка помещается в новый объект Task<string> и возвращается вызывающему коду.
Благодаря новой реализации метода DoWorkAsync() мы можем получить некоторое представление о подлинной роли ключевого слова await. Оно всегда будет модифицировать метод, который возвращает объект Task. Когда поток выполнения достигает await, вызывающий поток приостанавливается до тех пор, пока вызов не будет завершен. Запустив эту версию приложения, вы обнаружите, что сообщение Completed отображается перед сообщением Done with work! В случае графического приложения можно было бы продолжать работу с пользовательским интерфейсом одновременно с выполнением метода DoWorkAsync().
Класс SynchronizationContext и async/await
Тип SynchronizationContext формально определен как базовый класс, который предоставляет свободный от потоков контекст баз синхронизации. Хотя такое первоначальное определение не особо информативно, в официальной документации указаны следующие сведения.
Цель модели синхронизации, реализуемой классом SynchronizationContext, заключается в том, чтобы позволить внутренним асинхронным/синхронным операциям общеязыковой исполняющей среды вести себя надлежащим образом с различными моделями синхронизации.
Наряду с тем, что вам уже известно о многопоточности, такое заявление проливает свет на этот вопрос. Вспомните, что приложения с графическим пользовательским интерфейсом (Windows Forms, WPF) не разрешают прямой доступ к элементам управления из вторичных потоков, а требуют делегирования доступа. Вы уже видели объект Dispatcher в примере приложения WPF. В консольных приложениях, которые не используют WPF, это ограничение отсутствует. Речь идет о разных моделях синхронизации. С учетом всего сказанного давайте рассмотрим класс SynchronizationContext.
Класс SynchonizationContext является типом, предоставляющим виртуальный метод отправки, который принимает делегат, предназначенный для выполнения асинхронным образом. В результате инфраструктуры получают шаблон для надлежащей обработки асинхронных запросов (диспетчеризация для приложений WPF/Windows Forms, прямое выполнение для приложений без графического пользовательского интерфейса и т.д.). Он предлагает способ постановки в очередь единицы работы в контексте и подсчета асинхронных операций, ожидающих выполнения.
Как обсуждалось ранее, когда делегат помещается в очередь для асинхронного выполнения, он планируется к запуску в отдельном потоке, что обрабатывается средой .NET Core Runtime. Задача обычно решается с помощью управляемого пула потоков .NET Core Runtime, но может быть построена и специальная реализация.
Хотя такими связующими действиями можно управлять вручную в коде, шаблон async/await делает большую часть трудной работы. В случае применения await к асинхронному методу задействуются реализации SynchronizationContext и TaskScheduler целевой инфраструктуры. Например, если вы используете