Běžné chyby async/await

Async/await zjednodušuje asynchronní programování, ale některé chyby se objevují opakovaně. Tento článek popisuje pět nejběžnějších chyb v asynchronním kódu a ukazuje, jak jednotlivé chyby opravit.

Asynchronní metoda běží synchronně

Přidání klíčového async slova do metody nespustí metodu ve vlákně na pozadí. Říká kompilátoru, aby povolil await uvnitř těla metody a zabalil návratovou hodnotu do Task. Když vyvoláte asynchronní metodu, spustí se synchronně, dokud nedosáhne prvního await na neúplném awaitable. Pokud metoda neobsahuje žádné await výrazy, nebo pokud je každý čekatelný objekt, který očekává, již dokončen, metoda se zcela dokončí na volajícím vlákně.

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

V této metodě se okamžitě vrátí dokončený úkol, protože nikdy nepřináší. Kompilátor vygeneruje upozornění, když asynchronní metoda nemá await výrazy.

Pokud vaším cílem je přesměrovat zátěž vázanou na procesor do fondu vláken, použijte Run místo 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

Další pokyny k použití Task.Runnajdete v tématu Asynchronní obálky pro synchronní metody.

Nelze očekávat asynchronní metodu void.

Při převodu synchronní void-návratové metody na asynchronní, změňte návratový typ na Task. Pokud ponecháte návratový typ jako void, metoda se stane asynchronním voidem, který nemůžete očekávat:

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

Asynchronní metody void slouží ke konkrétnímu účelu: obslužné rutiny událostí nejvyšší úrovně v architekturách uživatelského rozhraní. Mimo obslužné rutiny událostí vždy vracet Task nebo Task<T> z asynchronních metod. Asynchronní metody void mají tyto nevýhody:

  • Výjimky zůstanou nepovšimnuty. Výjimky vyvolané v asynchronní metodě void se šíří do SynchronizationContext, který byl aktivní, když metoda začala. Volající nemůže tyto výjimky zachytit.
  • Volající nemůžou sledovat dokončení. Bez , Taskneexistuje žádný mechanismus, který by věděl, kdy se operace dokončí.
  • Testování je obtížné. Nemůžete použít `await` v testu k ověření chování metody.

Záseky z blokování v asynchronním kódu

Tato chyba je nejčastější příčinou asynchronního kódu, který se nikdy nedokončí. K tomu dochází, když synchronně blokujete (volání Wait, Task<TResult>.Resultnebo GetAwaiter.GetResult) ve vlákně, které má jednovláknové SynchronizationContext.

Posloupnost, která způsobuje zablokování:

  1. Kód ve vlákně uživatelského rozhraní (nebo vlákno požadavku ASP.NET ve starším ASP.NET) volá asynchronní metodu a blokuje vrácenou úlohu.
  2. Asynchronní metoda očekává neúplnou úlohu bez použití ConfigureAwait(false).
  3. Po dokončení očekávaného úkolu se pokračování pokusí odeslat zpět na původní SynchronizationContext.
  4. Vlákno daného kontextu je blokováno čekáním na dokončení úlohy – vzájemné zablokování.
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

Jak se vyhnout zablokování

Použijte jednu nebo více z těchto strategií:

  • Neblokujte. Použijte await místo .Result nebo .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
    
  • Použijte ConfigureAwait(false) v kódu knihovny. Pokud vaše metoda knihovny nepotřebuje pokračovat v kontextu volajícího, zadejte ConfigureAwait(false) u každého 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
    

    Použití ConfigureAwait(false) signalizuje modulu runtime, aby nepřenesl pokračování do původního SynchronizationContext. Tento přístup chrání blokující vlákna a zvyšuje výkon tím, že se vyhnete zbytečnému přepínání vláken.

Výstraha

Statický konstruktor se zablokuje. CLR drží zámek při spouštění statických konstruktorů (cctors). Pokud statický konstruktor blokuje úlohu a pokračování úkolu musí spouštět kód ve stejném typu (nebo typu zapojeného do stavebního řetězce), pokračování nemůže pokračovat, protože cctor zámek je uložený. Vyhněte se blokování volání uvnitř statických konstruktorů úplně.

Rozbalení úkolu<>

Když předáte asynchronní lambda výraz metodě, jako je StartNew, vrácený objekt je Task<Task> (nebo Task<Task<TResult>>), nikoli jednoduchý Task. Vnější úkol se dokončí, jakmile asynchronní lambda dosáhne prvního výnosu await. Nečeká na dokončení vnitřního úkolu:

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

Tento problém opravte jedním ze tří způsobů:

  • Místo toho použijte Run. Task.Run automaticky rozbaluje 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
    
  • Zavolejte Unwrap na výsledku:

    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
    
  • Čekejte dvakrát (nejprve vnější úkol, pak vnitřní):

    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
    

Chybějící funkce Await u volání vracejícího úkol

Pokud zavoláte metodu vracející úlohu v async metodě bez čekání na ni, metoda spustí asynchronní operaci, ale nečeká na její dokončení. Kompilátor vás upozorní na tento případ s CS4014 v jazyce C# a BC42358 v 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

Uložení výsledku do proměnné potlačí upozornění, ale neopraví základní chybu. Vždy await úkol, pokud záměrně nevyžadujete chování typu fire-and-forget.

Viz také