Распространенные ошибки async/await

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.

Последовательность, которая вызывает взаимоблокировку:

  1. Код в потоке пользовательского интерфейса (или потоке запроса ASP.NET в более ранних версиях ASP.NET) вызывает асинхронный метод и блокируется на возвращенной задаче.
  2. Асинхронный метод ожидает неполной задачи без использования ConfigureAwait(false).
  3. Когда ожидаемая задача завершится, продолжение пытается перейти обратно к исходному объекту SynchronizationContext.
  4. Поток контекста заблокирован в ожидании завершения задачи — взаимозаблокировка.
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, если вы не хотите намеренно использовать режим "запустил и забыл".

См. также