День двести девятнадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Окончание
Советы по использованию
1. Избегайте захвата контекста с помощью ConfigureAwait
По умолчанию при вызове await захватывается контекст текущего потока, чтобы продолжение метода исполнялось в том же потоке. При вызове асинхронного метода из потока UI – это то, что нужно.
Но если это библиотечный код (или код в приложении, которое не касается UI), не нужно возвращаться в поток UI, даже если метод вызван из него. Вообще, чем меньше кода выполняется в потоке UI, тем лучше.
Метод
2. Используйте параллельное выполнение, где это возможно.
Рассмотрим два примера. Этот:
3. Избегайте смешивания синхронного и асинхронного кода
Правильно реализовать логику, когда часть кода является синхронной, а другие части асинхронными, довольно сложно. Переключение между этими двумя подходами сопряжено с трудностями. Если у вас есть сетевая библиотека, которая предоставляет только синхронные операции, написать асинхронную оболочку для этих операций сложно, то же самое и наоборот.
В частности, следует помнить об опасности использования
4. Реализуйте отмену операции, где это возможно
Асинхронный код имеет преимущество перед синхронным ещё и в возможности отмены операции через токен отмены. Большинство асинхронных API предоставляют возможность передавать токен отмены в качестве параметра. Не пренебрегайте этой возможностью, даже если отмена не требуется на данном этапе разработки.
5. Тестирование асинхронного кода
Большинство фреймворков модульных тестов поддерживают асинхронные тесты:
При тестировании асинхронного кода часто требуется создать задачу, которая уже выполнена, с конкретным результатом или ошибкой. Здесь полезны методы
Для большей гибкости можно использовать
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
Многопоточность.
9. Async/await. Окончание
Советы по использованию
1. Избегайте захвата контекста с помощью ConfigureAwait
По умолчанию при вызове await захватывается контекст текущего потока, чтобы продолжение метода исполнялось в том же потоке. При вызове асинхронного метода из потока UI – это то, что нужно.
Но если это библиотечный код (или код в приложении, которое не касается UI), не нужно возвращаться в поток UI, даже если метод вызван из него. Вообще, чем меньше кода выполняется в потоке UI, тем лучше.
Метод
ConfigureAwait
принимает параметр, который определяет, будет ли ожидаемая операция захватывать контекст. На практике он почти всегда вызывается со значением false
:string text = await client.GetStringAsync(url).ConfigureAwait(false);Результатом вызова
ConfigureAwait(false)
является то, что продолжение не будет запланировано для исходного контекста синхронизации; оно будет выполняться в потоке из пула.2. Используйте параллельное выполнение, где это возможно.
Рассмотрим два примера. Этот:
Task<decimal> rateTask = employee.GetRateAsync();И этот:
decimal rate = await rateTask;
Task<int> hoursTask = timeSheet.GetHoursAsync(employee.Id);
int hours = await hoursTask;
AddPayment(rate * hours);
Task<decimal> rateTask = employee.GetRateAsync();В дополнение к тому, что второй кусок кода короче, он вводит параллелизм. Обе задачи могут быть запущены независимо, потому что вам не нужен результат первой задачи для выполнения второй. Это не означает, что асинхронная инфраструктура создаст больше потоков. Например, если две асинхронные операции являются веб-службами, оба запроса к веб-службам могут выполняться без блокировки каких-либо потоков.
Task<int> hoursTask = timeSheet.GetHoursAsync(employee.Id);
AddPayment(await rateTask * await hoursTask);
3. Избегайте смешивания синхронного и асинхронного кода
Правильно реализовать логику, когда часть кода является синхронной, а другие части асинхронными, довольно сложно. Переключение между этими двумя подходами сопряжено с трудностями. Если у вас есть сетевая библиотека, которая предоставляет только синхронные операции, написать асинхронную оболочку для этих операций сложно, то же самое и наоборот.
В частности, следует помнить об опасности использования
Task<TResult>.Result
и Task.Wait()
для синхронного получения результата асинхронной операции. Это может легко привести к тупику (deadlock). Чаще всего, когда асинхронная операция требует выполнения продолжения в заблокированном потоке, вызвавшем, например, Task.Wait()
.4. Реализуйте отмену операции, где это возможно
Асинхронный код имеет преимущество перед синхронным ещё и в возможности отмены операции через токен отмены. Большинство асинхронных API предоставляют возможность передавать токен отмены в качестве параметра. Не пренебрегайте этой возможностью, даже если отмена не требуется на данном этапе разработки.
5. Тестирование асинхронного кода
Большинство фреймворков модульных тестов поддерживают асинхронные тесты:
[Test]Среды тестирования часто предоставляют метод
public async Task FooAsync() {…}
Assert.ThrowsAsync
для проверки того, что вызов асинхронного метода возвращает задачу, которая завершается с ошибкой.При тестировании асинхронного кода часто требуется создать задачу, которая уже выполнена, с конкретным результатом или ошибкой. Здесь полезны методы
Task.FromResult
, Task.FromException
и Task.FromCanceled
.Для большей гибкости можно использовать
TaskCompletionSource<TResult>
. Этот тип позволяет вам создать задачу, представляющую текущую операцию, а затем установить результат (включая любое исключение или отмену) позже, таким образом завершив задачу. Это чрезвычайно полезно, когда вы хотите вернуть задачу из mock-зависимости, но сделать так, чтобы она завершалась позднее в коде теста.Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.