(вместо операторов верхнего уровня), тогда должны пометить метод с помощью ключевого слова
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
целевой инфраструктуры. Например, если вы используете
async/await
в приложении WPF, то инфраструктура WPF обеспечит диспетчеризацию делегата и обратный вызов в конечном автомате при завершении ожидающей задачи, чтобы безопасным образом обновить элементы управления.
Роль метода ConfigureAwait
Теперь, когда вы лучше понимаете роль класса
SynchronizationContext
, пришло время раскрыть роль метода
ConfigureAwait
. По умолчанию применение
await
к объекту
Task
приводит к использованию контекста синхронизации. При разработке приложений с графическим пользовательским интерфейсом (Windows Forms, WPF) именно такое поведение является желательным. Однако в случае написания кода приложения без графического пользовательского интерфейса накладные расходы, связанные с постановкой в очередь исходного контекста, когда в этом нет нужды, потенциально могут вызвать проблемы с производительностью приложения.
Чтобы увидеть все в действии, модифицируйте операторы верхнего уровня, как показано ниже: