Наряду с тем, что вам уже известно о многопоточности, такое заявление проливает свет на этот вопрос. Вспомните, что приложения с графическим пользовательским интерфейсом (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) именно такое поведение является желательным. Однако в случае написания кода приложения без графического пользовательского интерфейса накладные расходы, связанные с постановкой в очередь исходного контекста, когда в этом нет нужды, потенциально могут вызвать проблемы с производительностью приложения.
Чтобы увидеть все в действии, модифицируйте операторы верхнего уровня, как показано ниже:
Console.WriteLine(" Fun With Async ===>");
// Console.WriteLine(DoWork());
string message = await DoWorkAsync();
Console.WriteLine(message);
string message1 = await DoWorkAsync().ConfigureAwait(false);
Console.WriteLine(message1);
В исходном блоке кода применяется класс
SynchronizationContext
, поставляемый инфраструктурой (в данном случае средой .NET Core Runtime), что эквивалентно вызову
ConfigureAwait(true)
. Во втором примере текущий контекст и планировщик игнорируются.
Согласно рекомендациям команды создателей .NET Core при разработке прикладного кода (Windows Forms, WPF и т.д.) следует полагаться на стандартное поведение, а в случае написания неприкладного кода (скажем, библиотеки) использовать вызов
ConfigureAwait(false)
. Одним исключением является инфраструктура ASP.NET Core (рассматриваемая в части IX), где специальная реализация
SynchronizationContext
не создается; таким образом, вызов
ConfigureAwait(false)
не дает преимущества при работе с другими инфраструктурами.
Соглашения об именовании асинхронных методов
Конечно же, вы заметили, что мы изменили имя метода с
DoWork()
на
DoWorkAsync()
, но по какой причине? Давайте предположим, что новая версия метода по-прежнему называется
DoWork()
, но вызывающий код реализован так:
// Отсутствует ключевое слово await!
string message = DoWork();
Обратите внимание, что мы действительно пометили метод ключевым словом
async
, но не указали ключевое слово await при вызове
DoWork()
. Здесь мы получим ошибки на этапе компиляции, потому что возвращаемым значением
DoWork()
является объект
Task
, который мы пытаемся напрямую присвоить переменной типа
string
. Вспомните, что ключевое слово
await
отвечает за извлечение внутреннего возвращаемого значения, которое содержится в объекте
Task
. Поскольку
await
отсутствует, возникает несоответствие типов.
На заметку! Метод, поддерживающий
await
— это просто метод, который возвращает
Task
или
Task<T>
.
С учетом того, что методы, которые возвращают объекты
Task
, теперь могут вызываться в неблокирующей манере посредством конструкций
async
и
await
, в Microsoft рекомендуют (в качестве установившейся практики) снабжать имя любого метода, возвращающего
Task
, суффиксом
Async
. В таком случае разработчики, которым известно данное соглашение об именовании, получают визуальное напоминание о том, что ключевое слово
await
является обязательным, если они намерены вызывать метод внутри асинхронного контекста.
На заметку! Обработчики событий для элементов управления графического пользовательского интерфейса (вроде обработчика события
Click
кнопки), а также методы действий внутри приложений в стиле MVC, к которым применяются ключевые слова
async
и
await
, не следуют указанному соглашению об именовании.
Асинхронные методы, возвращающие void
В настоящий момент наш метод
DoWorkAsync()
возвращает объект
Task
, содержащий "реальные данные" для вызывающего кода, которые будут получены прозрачным образом через ключевое слово
await
. Однако что если требуется построить асинхронный метод, возвращающий
void
? Реализация зависит о того, нуждается метод в применении
await
или нет (как в сценариях "запустил и забыл").