Häufige asynchrone/await-Fehler

Async/await vereinfacht die asynchrone Programmierung, aber bestimmte Fehler werden wiederholt angezeigt. In diesem Artikel werden die fünf häufigsten Fehler im asynchronen Code beschrieben und erläutert, wie Sie die einzelnen Fehler beheben.

Async-Methode wird synchron ausgeführt

Wenn Sie das async Schlüsselwort zu einer Methode hinzufügen, wird die Methode nicht in einem Hintergrundthread ausgeführt. Er weist den Compiler an, await innerhalb des Methodentexts zuzulassen und den Rückgabewert in einem Task zu verpacken. Wenn Sie eine asynchrone Methode aufrufen, wird diese synchron ausgeführt, bis sie bei einem unvollständigen Awaitable das erste await erreicht. Wenn die Methode keine await-Ausdrücke enthält oder wenn alle Awaitables, auf die sie wartet, bereits abgeschlossen sind, wird die Methode vollständig auf dem aufrufenden Thread abgeschlossen:

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 gibt die Methode sofort eine abgeschlossene Aufgabe zurück, da sie nie anhält (yield). Der Compiler gibt eine Warnung aus, wenn eine async Methode keine await Ausdrücke enthält.

Wenn Ihr Ziel darin besteht, CPU-gebundene Arbeit in einen Threadpoolthread zu entladen, verwenden Sie Run anstelle von 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

Weitere Anleitungen zur Verwendung Task.Runfinden Sie unter "Asynchrone Wrapper für synchrone Methoden".

Auf eine asynchrone Void-Methode kann nicht gewartet werden.

Wenn Sie eine synchrone void-returning-Methode in asynchron konvertieren, ändern Sie den Rückgabetyp in Task. Wenn Sie für den Rückgabetyp void belassen, wird die Methode zu „async void“, auf die Sie nicht warten können:

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

Async void-Methoden dienen einem bestimmten Zweck: Ereignishandler der obersten Ebene in UI-Frameworks. Außerhalb von Ereignishandlern sollten Sie immer Task oder Task<T> von asynchronen Methoden zurückgeben. Async void-Methoden haben folgende Nachteile:

  • Ausnahmen werden nicht überwacht. Ausnahmen, die in einer asynchronen void-Methode ausgelöst wurden, werden an das SynchronizationContext , das aktiv war, wenn die Methode gestartet wurde, weitergegeben. Der Aufrufer kann diese Ausnahmen nicht abfangen.
  • Anrufer können den Abschluss nicht nachverfolgen. Ohne einen Task gibt es keinen Mechanismus, um zu wissen, wann der Vorgang abgeschlossen ist.
  • Tests sind schwierig. Sie können die Methode in einem Test nicht aufrufen, um ihr Verhalten zu überprüfen.

Deadlocks aufgrund von Blockierungen bei asynchronem Code

Dieser Fehler ist die häufigste Ursache dafür, dass asynchroner Code „niemals abgeschlossen wird“. Er tritt auf, wenn Sie in einem Thread synchron blockieren (Wait, Task<TResult>.Result oder GetAwaiterGetResultaufrufen), dies in einem Thread, der über ein Single-Thread-SynchronizationContext verfügt.

Die Sequenz, die einen Deadlock verursacht:

  1. Code auf dem UI-Thread (oder einem ASP.NET-Anforderungs-Thread in älteren ASP.NET-Versionen) ruft eine asynchrone Methode auf und wartet auf die zurückgegebene Aufgabe.
  2. Die asynchrone Methode wartet auf eine unvollständige Aufgabe, ohne Verwendung von ConfigureAwait(false).
  3. Wenn die erwartete Aufgabe abgeschlossen ist, versucht die Fortsetzung, eine Rückmeldung an das ursprüngliche SynchronizationContext zu senden.
  4. Der Thread dieses Kontexts wird blockiert, bis die Aufgabe abgeschlossen ist – Deadlock.
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

So vermeiden Sie Deadlocks

Verwenden Sie eine oder mehrere dieser Strategien:

  • Blockieren Sie nicht. Verwenden Sie await anstelle von .Result oder .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
    
  • Verwenden Sie ConfigureAwait(false) im Bibliothekscode. Wenn Ihre Bibliotheksmethode nicht im Kontext des Aufrufers fortgesetzt werden muss, geben Sie an jeder Stelle, an der ConfigureAwait(false) steht, await an:

    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
    

    Die Verwendung von ConfigureAwait(false) weist die Laufzeitumgebung an, die Fortsetzung nicht auf das ursprüngliche SynchronizationContext zurückzuverweisen. Dieser Ansatz schützt Anrufer, die blockieren, und verbessert die Leistung, indem unnötige Threadhüpfungen vermieden werden.

Warnung

Statische Konstruktor-Deadlocks. Die CLR hält eine Sperre, während statische Konstruktoren (cctors) ausgeführt werden. Wenn ein statischer Konstruktor eine Aufgabe blockiert und die Fortsetzung dieser Aufgabe Code desselben Typs (oder eines Typs, der in die Konstruktionskette involviert ist) ausführen muss, kann die Fortsetzung nicht fortfahren, da die Sperre cctor gehalten wird. Vermeiden Sie das Blockieren von Aufrufen innerhalb statischer Konstruktoren vollständig.

Aufgabe<Aufgabe> entpacken

Wenn Sie eine asynchrone Lambda-Funktion an eine Methode wie StartNew übergeben, ist das zurückgegebene Objekt ein Task<Task> (oder Task<Task<TResult>>), kein einfaches Task-Objekt. Die äußere Aufgabe wird abgeschlossen, sobald der asynchrone Lambda-Ausdruck auf die erste yielding await trifft. Es wartet nicht, bis der innere Vorgang abgeschlossen ist:

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

Beheben Sie dieses Problem auf drei Arten:

  • Verwenden Sie stattdessen Run. Task.Run automatisch entpackt 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
    
  • Rufen Sie Unwrap für das Ergebnis auf:

    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
    
  • Warten Sie zweimal (zuerst die äußere Aufgabe, dann das innere):

    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
    

Fehlende Wartezeit bei einem Aufruf, der eine Aufgabe zurückgibt

Wenn Sie eine Task-Returning-Methode in einer async Methode aufrufen, ohne darauf zu warten, startet die Methode den asynchronen Vorgang, wartet jedoch nicht darauf, bis sie abgeschlossen ist. Der Compiler warnt Sie bei diesem Fall mit CS4014 in C# und 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

Durch das Speichern des Ergebnisses in einer Variablen wird die Warnung unterdrückt, der zugrunde liegende Fehler wird jedoch nicht behoben. Führen Sie die Aufgabe immer aus (await), es sei denn, Sie möchten bewusst ein „Fire-and-Forget“-Verhalten.

Siehe auch