Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Async/await упрощает асинхронное программирование, но некоторые ошибки появляются неоднократно. В этой статье описываются пять наиболее распространенных ошибок в асинхронном коде и показано, как исправить каждую из них.
Асинхронный метод запускается в синхронном режиме
Добавление ключевого слова async к методу не позволяет методу выполняться в фоновом потоке. Он сообщает компилятору разрешить await внутри текста метода и упаковать возвращаемое значение в объект Task. При вызове асинхронного метода он выполняется синхронно до тех пор, пока не достигнет первого await, связанного с незавершённым ожидаемым объектом. Если метод не содержит выражений await, или если все ожидаемые объекты, которых он ожидает, уже завершены, метод полностью завершается в вызывающем потоке:
public static class SyncExecutionExample
{
public static Task<int> ComputeAsync()
{
// No await in this method — it runs entirely synchronously.
return Task.FromResult(42);
}
}
Public Module SyncExecutionExample
Public Function ComputeAsync() As Task(Of Integer)
' No Await in this method — it runs entirely synchronously.
Return Task.FromResult(42)
End Function
End Module
Здесь метод возвращает завершенную задачу немедленно, так как она никогда не дает. Компилятор выдает предупреждение, когда асинхронный метод не имеет await выражений.
Если ваша цель состоит в том, чтобы выгрузить вычисления, привязанные к ЦП, в поток пула потоков, используйте Run вместо async:
public static class OffloadExample
{
public static int ComputeIntensive()
{
int sum = 0;
for (int i = 0; i < 1_000; i++)
sum += i;
return sum;
}
public static Task<int> ComputeOnThreadPoolAsync()
{
return Task.Run(() => ComputeIntensive());
}
}
Public Module OffloadExample
Public Function ComputeIntensive() As Integer
Dim sum As Integer = 0
For i As Integer = 0 To 999
sum += i
Next
Return sum
End Function
Public Function ComputeOnThreadPoolAsync() As Task(Of Integer)
Return Task.Run(Function() ComputeIntensive())
End Function
End Module
Дополнительные сведения об использовании Task.Run см. в статье Асинхронные оболочки для синхронных методов.
Невозможно ожидать метода void с асинхронным выполнением
При преобразовании синхронного метода void, возвращающего значение, в асинхронный, измените возвращаемый тип на Task. Если вы оставьте возвращаемый тип как void, метод становится "async void", который нельзя ожидать:
public static class AsyncVoidExample
{
// BAD: async void — can't be awaited.
public static async void DoWorkBadAsync()
{
await Task.Delay(100);
}
// GOOD: async Task — callers can await this.
public static async Task DoWorkGoodAsync()
{
await Task.Delay(100);
}
}
Public Module AsyncVoidExample
' BAD: Async Sub — can't be awaited.
Public Async Sub DoWorkBadAsync()
Await Task.Delay(100)
End Sub
' GOOD: Async Function returning Task — callers can await this.
Public Async Function DoWorkGoodAsync() As Task
Await Task.Delay(100)
End Function
End Module
Асинхронные методы void служат определенной целью: обработчики событий верхнего уровня в платформах пользовательского интерфейса. Всегда возвращайте Task или Task<T> из асинхронных методов вне обработчиков событий. Асинхронные методы void имеют следующие недостатки:
- Исключения остаются незамеченными. Исключения, создаваемые в асинхронном методе с возвращаемым типом void, распространяются на SynchronizationContext, активный во время запуска метода. Вызывающий не может перехватывать эти исключения.
- Вызывающие не могут отслеживать завершение.
TaskБез него нет механизма, чтобы узнать, когда операция завершится. - Тестирование сложно. Вы не можете дождаться метода в тесте, чтобы проверить его поведение.
Взаимоблокировки из-за блокировки в асинхронном коде
Эта ошибка является наиболее распространенной причиной асинхронного кода, который "никогда не завершается". Это происходит при синхронном блокировании (вызове Wait, Task<TResult>.Result или GetAwaiter.GetResult) в потоке с однопоточным SynchronizationContext.
Последовательность, которая вызывает взаимоблокировку:
- Код в потоке пользовательского интерфейса (или потоке запроса ASP.NET в более ранних версиях ASP.NET) вызывает асинхронный метод и блокируется на возвращенной задаче.
- Асинхронный метод ожидает неполной задачи без использования
ConfigureAwait(false). - Когда ожидаемая задача завершится, продолжение пытается перейти обратно к исходному объекту
SynchronizationContext. - Поток контекста заблокирован в ожидании завершения задачи — взаимозаблокировка.
public static class DeadlockExample
{
public static async Task<string> GetDataAsync()
{
// Without ConfigureAwait(false), this continuation
// posts back to the original SynchronizationContext.
await Task.Delay(100);
return "data";
}
public static void CallerThatDeadlocks()
{
// On a single-threaded SynchronizationContext (e.g. UI thread),
// the following line deadlocks because the continuation needs
// the same thread that .Result is blocking.
string result = GetDataAsync().Result;
}
}
Public Module DeadlockExample
Public Async Function GetDataAsync() As Task(Of String)
' Without ConfigureAwait(False), this continuation
' posts back to the original SynchronizationContext.
Await Task.Delay(100)
Return "data"
End Function
Public Sub CallerThatDeadlocks()
' On a single-threaded SynchronizationContext (e.g. UI thread),
' the following line deadlocks because the continuation needs
' the same thread that .Result is blocking.
Dim result As String = GetDataAsync().Result
End Sub
End Module
Как избежать взаимоблокировок
Используйте одну или несколько из следующих стратегий:
Не блокируйте. Используйте
awaitвместо.Resultили.Wait():public static class DeadlockFix1 { public static async Task CallerFixedAsync() { // Use await instead of .Result string result = await DeadlockExample.GetDataAsync(); Console.WriteLine(result); } }Public Module DeadlockFix1 Public Async Function CallerFixedAsync() As Task ' Use Await instead of .Result Dim result As String = Await DeadlockExample.GetDataAsync() Console.WriteLine(result) End Function End ModuleИспользуйте
ConfigureAwait(false)в коде библиотеки. Если методу библиотеки не нужно возобновляться в контексте вызывающего, укажитеConfigureAwait(false)в каждомawait.public static class DeadlockFix2 { public static async Task<string> GetDataSafeAsync() { await Task.Delay(100).ConfigureAwait(false); return "data"; } }Public Module DeadlockFix2 Public Async Function GetDataSafeAsync() As Task(Of String) Await Task.Delay(100).ConfigureAwait(False) Return "data" End Function End ModuleИспользование
ConfigureAwait(false)указывает среде выполнения не выполнять маршализацию продолжения обратно в исходный объектSynchronizationContext. Этот подход защищает вызовы, которые блокируют, и повышает производительность, избегая ненужных переключений потоков.
Предупреждение
Взаимоблокировки статического конструктора. CLR удерживает блокировку во время выполнения статических конструкторов (cctors). Если статический конструктор блокирует задачу и продолжение задачи должно выполнять код в том же типе (или типе, связанном с цепочкой конструирования), продолжение не может выполниться, так как cctor блокировка удерживается. Полностью избегайте блокировки вызовов внутри статических конструкторов.
<Распаковка задач>
При передаче асинхронной лямбды в метод, например StartNew, возвращаемый объект является Task<Task> (или Task<Task<TResult>>), а не простым Task. Внешняя задача завершается, как только асинхронная лямбда-функция достигает своего первого yield-пункта await. Он не ожидает завершения внутренней задачи:
public static class TaskTaskBugExample
{
public static async Task DemoAsync()
{
var sw = Stopwatch.StartNew();
// StartNew returns Task<Task>, not Task.
// The outer task completes immediately when the lambda yields.
await Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
});
// Elapsed shows ~0 seconds, not ~1 second.
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module TaskTaskBugExample
Public Async Function DemoAsync() As Task
Dim sw = Stopwatch.StartNew()
' StartNew returns Task(Of Task), not Task.
' The outer task completes immediately when the lambda yields.
Await Task.Factory.StartNew(Async Function()
Await Task.Delay(1000)
End Function)
' Elapsed shows ~0 seconds, not ~1 second.
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s")
End Function
End Module
Исправьте эту проблему одним из трех способов:
Вместо этого используйте Run.
Task.Runавтоматическая распаковкаTask<Task>:public static class TaskTaskFix1 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); await Task.Run(async () => { await Task.Delay(1000); }); Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix1 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Await Task.Run(Async Function() Await Task.Delay(1000) End Function) Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End ModuleПрименить Unwrap к результату:
public static class TaskTaskFix2 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); await Task.Factory.StartNew(async () => { await Task.Delay(1000); }).Unwrap(); Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix2 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Await Task.Factory.StartNew(Async Function() Await Task.Delay(1000) End Function).Unwrap() Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End ModuleОжидать дважды (сначала внешняя задача, потом внутренняя):
public static class TaskTaskFix3 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); Task<Task> outerTask = Task.Factory.StartNew(async () => { await Task.Delay(1000); }); Task innerTask = await outerTask; await innerTask; Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix3 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Dim outerTask As Task(Of Task) = Task.Factory.StartNew(Async Function() Await Task.Delay(1000) End Function) Dim innerTask As Task = Await outerTask Await innerTask Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End Module
Отсутствует ожидание вызова, возвращающего задачу
Если вы вызываете метод, возвращающий задачу, в методе async, не дожидаясь его выполнения, метод запускает асинхронную операцию, но не ожидает ее завершения. Компилятор предупреждает вас об этом случае с CS4014 в C# и BC42358 в Visual Basic:
public static class MissingAwaitExample
{
// BAD: Task.Delay is started but never awaited.
public static async Task PauseOneSecondBuggyAsync()
{
Task.Delay(1000); // CS4014 warning
}
// GOOD: await the task.
public static async Task PauseOneSecondAsync()
{
await Task.Delay(1000);
}
}
Public Module MissingAwaitExample
' BAD: Task.Delay is started but never awaited.
Public Async Function PauseOneSecondBuggyAsync() As Task
Task.Delay(1000) ' Warning BC42358
End Function
' GOOD: Await the task.
Public Async Function PauseOneSecondAsync() As Task
Await Task.Delay(1000)
End Function
End Module
Сохранение результата в переменной подавляет предупреждение, но не исправляет базовую ошибку. Всегда выполняйте задачу await, если вы не хотите намеренно использовать режим "запустил и забыл".