Veelvoorkomende async/await fouten

Async/await vereenvoudigt asynchrone programmering, maar bepaalde fouten worden herhaaldelijk weergegeven. In dit artikel worden de vijf meest voorkomende fouten in asynchrone code beschreven en wordt beschreven hoe u deze kunt oplossen.

De Async-methode wordt synchroon uitgevoerd

Als u het async trefwoord aan een methode toevoegt, wordt de methode niet uitgevoerd op een achtergrondthread. Het geeft aan de compiler door dat await in de methodebody mag staan en dat de retourwaarde moet worden verpakt in een Task. Wanneer u een asynchrone methode aanroept, wordt deze synchroon uitgevoerd totdat de eerste await op een onvolledig wachtbaar object is bereikt. Als de methode geen await expressies bevat of als alle awaitables al zijn voltooid, voltooit de methode volledig op de aanroepende thread.

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

Hier retourneert de methode onmiddellijk een voltooide taak, omdat deze nooit oplevert. De compiler verzendt een waarschuwing wanneer een asynchrone methode geen expressies await bevat.

Als u CPU-gebonden werk wilt verplaatsen naar een thread in de threadgroep, gebruikt u Run in plaats van 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

Voor meer informatie over wanneer Task.Run gebruikt moeten worden, zie Asynchrone wrappers voor synchrone methoden.

Kan geen async void-methode afwachten

Wanneer u een synchrone voidretourmethode converteert naar asynchroon, wijzigt u het retourtype in Task. Als u het retourtype opgeeft als void, wordt de methode 'asynchrone leegte', die u niet kunt wachten:

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

Asynchrone void-methoden dienen voor een specifiek doel: gebeurtenis-handlers op het hoogste niveau in UI-frameworks. Buiten gebeurtenis-handlers retourneert u altijd Task of Task<T> van asynchrone methoden. Asynchrone void-methoden hebben deze nadelen:

  • Uitzonderingen blijven onopgemerkt. Uitzonderingen die zijn opgetreden in een asynchrone ongeldige methode worden doorgegeven aan de SynchronizationContext methode die actief was toen de methode werd gestart. De aanroeper kan deze uitzonderingen niet ondervangen.
  • Bellers kunnen de voltooiing niet volgen. Zonder een Task, is er geen mechanisme om te weten wanneer de bewerking is voltooid.
  • Testen is moeilijk. U kunt in een test de methode niet afwachten om het gedrag ervan te verifiëren.

Impasses blokkeren voor asynchrone code

Deze fout is de meest voorkomende oorzaak van asynchrone code die nooit is voltooid. Dit gebeurt wanneer u synchroon blokkeert (aanroep Wait, Task<TResult>.Resultof GetAwaiter.GetResult) op een thread met één thread SynchronizationContext.

De volgorde die een impasse veroorzaakt:

  1. Code op de UI-thread (of een ASP.NET aanvraagthread in oudere ASP.NET) roept een asynchrone methode aan en blokkeert voor de geretourneerde taak.
  2. De asynchrone methode wacht op een onvolledige taak zonder ConfigureAwait(false) te gebruiken.
  3. Wanneer de verwachte taak is voltooid, probeert de voortzetting terug te sturen naar het oorspronkelijke SynchronizationContext.
  4. De thread van die context wordt geblokkeerd totdat de taak is voltooid. Dit is een impasse.
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

Impasses voorkomen

Gebruik een of meer van deze strategieën:

  • Niet blokkeren. Gebruiken await in plaats van.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
    
  • Gebruik ConfigureAwait(false) in bibliotheekcode. Wanneer uw bibliotheekmethode niet hoeft te worden hervat in de context van de aanroeper, specificeert u ConfigureAwait(false) voor elke 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
    

    Door ConfigureAwait(false) te gebruiken vertelt men de runtime de voortzetting niet terug te leiden naar het oorspronkelijke SynchronizationContext. Deze aanpak beschermt bellers die blokkeren en verbetert de prestaties door onnodige threadhops te voorkomen.

Waarschuwing

Statische constructor deadlock. De CLR heeft een vergrendeling tijdens het uitvoeren van statische constructors (cctors). Als een statische constructor op een taak blokkeert en de voortzetting van die taak code moet uitvoeren in hetzelfde type (of een type dat betrokken is bij de constructieketen), kan de voortzetting niet doorgaan omdat de cctor-vergrendeling vastgehouden wordt. Voorkom het volledig blokkeren van aanroepen binnen statische constructors.

Taaktaak<> uitpakken

Wanneer u een asynchrone lambda doorgeeft aan een methode zoals StartNew, is het geretourneerde object een Task<Task> (of Task<Task<TResult>>), niet een eenvoudige Task. De buitenste taak wordt voltooid zodra de asynchrone lambda de eerste opbrengst bereikt await. Er wordt niet gewacht totdat de interne taak is voltooid.

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

Los dit probleem op drie manieren op:

  • Gebruik in plaats daarvan Run. Task.Run pakt Task<Task> automatisch uit:

    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
    
  • Roep Unwrap het resultaat aan:

    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
    
  • Wacht tweemaal (eerst de buitenste taak, vervolgens de binnenste):

    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
    

Ontbrekende wacht op een aanroep voor het retourneren van taken

Als u een methode voor het retourneren van taken aanroept in een async methode zonder erop te wachten, start de methode een asynchrone bewerking, maar wacht niet totdat deze is voltooid. De compiler waarschuwt u voor dit geval met CS4014 in C# en BC42358 in 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

Als u het resultaat opslaat in een variabele, wordt de waarschuwing onderdrukt, maar wordt de onderliggende fout niet opgelost. Altijd await de taak uitvoeren, tenzij u specifiek het "fire-and-forget"-gedrag wilt gebruiken.

Zie ook