Асинхронное программирование в C#

Небольшой конспект доклада об асинхронности в .NET. Cоветую его посмотреть, будет полезен как новичкам, так и уже опытным ребятам.

Что такое асинхронность?

Асинхронность — выполнение программного кода, не блокирующее потоки во время ожидания.

Например: делаем запрос в бд для получения некоторых данных.
У нас есть поток и запрос, который выполняет этот поток. При синхронном выполнении наш поток будет ждать, пока БД не ответит. В асинхронном случае наш поток будет выполнять другой запрос, пока БД обрабатывает текущий.

TAP

С приходом .NET Framework 4.0 появился новый способ для работы с асинхронностью — TAP. По сути, это набор классов, которые предоставляют более удобный интерфейс для работы с асинхронным кодом. Но мы всё ещё можем использовать старые подходы: APM и EAP. Но Майкрософт советует использовать TAP.

Центральным классов в TAP является Task. Он описывает отдельную задачу, выполнение которой может завершиться в какой-то момент. 

Детальнее про подходы работы с многопоточностью.

Как создать задачу?

Фреймворк предоставляет множество способов для создания и запуска задач, а также оборачивания старого кода (APM и EAP).

Фабрики запущенных задач

Task.Run(Action/Func)
Task.Factory.StartNew(Action/Func)

Фабрики завершенных задач

Task.FromResult(Result)
Task.FromCanceled(Result)
Task.FromException(Result)
Task.CompletedTask

Конструктор

var task = new Task(Action/Func)
task.Start()

Фабрики для оборачивания в таски

Task.Factory.FromAsync(APM pattern)
TaskCompletionSource(EAP, APM, etc)

Класс TaskScheduler

Класс, который содержит стратегию запуска и планирования задач Task. Существуют две стандартные реализации: ThreadPoolTaskScheduler и SynchronizationContextTaskScheduler. Первая запускает задачи с помощью пула потоков, а вторая — использует текущий контекст синхронизации.

Чтобы указать планировщику, что наша задача продолжительная мы можем использовать флаг TaskCreationOptions.LongRunning (указывается при создании задачи). Это гарантирует, что под нашу задачу выделиться отдельный поток и в него не будут планироваться продолжения других операций.

У каждого потока есть свой «текущий» контекст, который необязательно должен быть уникальным. Иногда нам нужно, чтобы завершение или продолжение асинхронной операции выполнялись в том же контексте, откуда было запущено. Например, приложения с UI. После нажатия на кнопку мы запускаем асинхронную операцию, результат которой нужно вывести на экран. Но мы не сможем получить доступ к UI элементу, так как он находится в другом потоке. Для этого нам нужно запустить обновление данных в контексте UI потока. Именно для этого и нужен контекст синхронизации.

Контекст синхронизации при работе с Task и Thread

Комбинирование задач

Есть ситуации, когда нам нужно ожидать выполнения нескольких задач, для этого мы можем использовать комбинаторы задач.

Task.WaitAll();
Task.WaitAny();
Task.WhenAll();
Task.WhenAny();

Но нужно быть внимательным, так как WaitAll и WaitAny являются блокирующими операциями. Вместо них желательно использовать WhenAll и WhenAny.

Как получить результат?

Получить результат из операции можно несколькими способами. Некоторые из них являются блокирующими. Старайтесь избегать синхронного ожидания, так как это может привести к дедлокам.

Блокирующее получение результата:

.Result
.GetAwaiter().GetResult() - //лучше не использовать так как этот метод предназначен для компилятора, а не для нас.
.Wait()

Асинхронное:

await

Что такое sync over async deadlock?

Продолжение задач

Есть ситуации, когда нам нужно выполнить несколько операций одну за одной. Для этого у нас есть метод ContinueWith, с помощью которого можно построить цепочку продолжений.

Пример:

var task = Task.Run(() => Console.WriteLine("Async task"));
var nextTask = task.ContinueWith((prevTask) => {
	Console.WriteLine("Continuation");
});
nextTask.ContinueWith((prevTask) => {
	Console.WriteLine("Last Continuation");
});

Мы можем настраивать условия продолжения используя класс TaskContinuationOptions. С его помощью мы можем строить дерево задач, запускать продолжения по условиям и т. д.

Официальная документация

Обработка исключений

Работа с исключениями в асинхронном коде отличается от синхронного. Мы можем и не узнать об исключении, если не будем явно его обрабатывать. А в более старых версиях .NET это могло привести к падению приложения.

Обработка исключения при использовании await:

try
{
	await Task.Run(() => { throw new Exception(); });
}
catch (Exception e)
{
	// 
}

По умолчанию ContinueWith выполняется независимо от того произошло ли исключение в предыдущей задаче. Но мы можем его настроить двумя способами: в самом продолжении проверить предыдущую задачу или сделать отдельное продолжение, которое будет вызвано только при возникновении исключения.

Task.Run(() => { throw new Exception(); })
	.ContinueWith((prev) => {
		if (prev.IsFaulted)
		{
			//
		}
	});

Task.Run(() => { throw new Exception(); })
	.ContinueWith((prev) => {
		
	}, TaskCreationOptions.OnlyOnfaulted);

Стоит избегать асинхронных методов, которые ничего не возвращают (async void). Так как при возникновении исключения внутри метода, мы просто не сможем его обработать.

Комбинаторы задач также по-разному предоставляют исключения:

  • Task.WaitAll() — вернёт AggregateException;
  • Task.WhenAll() — вернёт только первое исключение;

Отмена задач

Если нам нужно отменить выполнение асинхронной задачи мы можем воспользоваться классом CancellationToken. Про него я рассказывал раньше.

async/await

Основная идея этого подхода заключается в написании асинхронного кода как синхронного. Это позволило создать более дружелюбный интерфейс для работы с асинхронностью.

Что делает async?

  • Создаёт машину состояний, которая обрабатывает все продолжения и синхронизации.
  • Разрешает использовать await.
  • Позволяет передавать вверх по стеку результат и исключения, используя Task.

Что делает await?

  • Позволяет не блокирующее ожидать результат.
  • Запуск продолжения в нужном потоке.
  • Возвращает результат или исключение.

Заблуждения по поводу await

  • Запускает операцию асинхронно.
  • Являться синтаксическим сахаром над Task.ContinueWtih.
  • Обязательно запускает продолжение в новый поток.
  • Всегда работает асинхронно.

Советы

  • Не используйте комбинацию async void — вы не сможете отловить исключение, а также ожидать выполнение метода.
  • Методы помеченные как async должны внутри себя содержать await.
  • Старайтесь вежде использовать не блокирующее ожидание await вместо блокирующего.
  • Для выполнения продолжительных операций используйте Task.Factory.StartNew вместе с TaskCreationOptions.LognRunning.
  • Используйте await t.ConfigureAwait(false) для библиотечного кода.
  • Не используйте совместно await и ContinueWith, так как они ничего не знают друг о друге.

Что осталось за кадром?

  • Асинхронные потоки
  • ValueTask
  • Progress

Ссылки

Поделиться
Отправить
Запинить
 52   23 дн   .net   c#
← Ctrl
Популярное