Vanliga async/await-buggar

Async/await förenklar asynkron programmering, men vissa misstag visas upprepade gånger. Den här artikeln beskriver de fem vanligaste buggarna i asynkron kod och visar hur du åtgärdar var och en.

Async-metoden körs synkront

Att lägga till nyckelordet async i en metod gör inte att metoden körs i en bakgrundstråd. Den instruerar kompilatorn att tillåta await inuti metodtexten och att omsluta returvärdet i en Task. När du anropar en asynkron metod körs den synkront tills den når den första await på en ofullständig inväntningsbar. Om metoden inte innehåller några await-uttryck, eller om varje awaitable-objekt redan är klart, slutförs metoden helt i den anropande tråden.

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

Här returnerar metoden en slutförd aktivitet omedelbart eftersom den aldrig ger resultat. Kompilatorn genererar en varning när en asynkron await metod saknar uttryck.

Om målet är att avlasta Cpu-bundet arbete till en tråd i trådpoolen använder du Run istället för 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

Mer information om när du ska använda Task.Runfinns i Asynkrona omslutningar för synkrona metoder.

Det går inte att vänta på en asynkron void-metod

När du konverterar en synkron void-returneringsmetod till asynkron ändrar du returtypen till Task. Om du lämnar returtypen som voidblir metoden "async void", vilket du inte kan vänta på:

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-metoder har ett specifikt syfte: händelsehanterare på högsta nivå i gränssnittsramverk. Utanför händelsehanterare ska du alltid returnera Task eller Task<T> från asynkrona metoder. Async void-metoder har följande nackdelar:

  • Undantagen förblir oobserverade. Undantag som genereras i en asynkron void-metod sprids till den SynchronizationContext som var aktiv när metoden startade. Anroparen kan inte fånga dessa undantag.
  • Uppringare kan inte spåra slutförandet. Utan Task finns det ingen mekanism för att veta när åtgärden har slutförts.
  • Testning är svårt. Du kan inte vänta på metoden i ett test för att verifiera dess beteende.

Dödlägen som uppstår vid blockering av asynkron programkod

Den här buggen är den vanligaste orsaken till asynkron kod som "aldrig slutförs". Det händer när du synkront blockerar (anropar Wait, Task<TResult>.Resulteller GetAwaiter.GetResult) på en tråd som har en enkeltrådad SynchronizationContext.

Sekvensen som orsakar ett dödläge:

  1. Kod i användargränssnittstråden (eller en ASP.NET begärandetråd i äldre ASP.NET) anropar en asynkron metod och blockerar den returnerade uppgiften.
  2. Metoden async väntar på en ofullständig uppgift utan att använda ConfigureAwait(false).
  3. När den väntade uppgiften är klar försöker fortsättningen att publicera tillbaka till den ursprungliga SynchronizationContext.
  4. Kontextens tråd blockeras i väntan på att uppgiften ska slutföras – dödläge.
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

Så här undviker du dödlägen

Använd en eller flera av dessa strategier:

  • Blockera inte. Använd await i stället för .Result eller .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
    
  • Använd ConfigureAwait(false) i bibliotekskod. När biblioteksmetoden inte behöver återupptas i anroparens kontext anger du ConfigureAwait(false) på varje 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
    

    Genom att använda ConfigureAwait(false) instruerar körningen att inte överföra fortsättningen till den ursprungliga SynchronizationContext. Den här metoden skyddar anropare som blockerar och förbättrar prestandan genom att undvika onödiga trådhopp.

Varning

Statisk konstruktordödläge. CLR har ett lås när statiska konstruktorer (cctors) körs. Om en statisk konstruktor blockerar en aktivitet och aktivitetens fortsättning måste köra kod av samma typ (eller en typ som ingår i byggkedjan) kan fortsättningen inte fortsätta eftersom låset cctor hålls. Undvik att blockera anrop i statiska konstruktorer helt.

Uppgift<Uppgift> avkodning

När du skickar en asynkron lambda till en metod som StartNewär det returnerade objektet en Task<Task> (eller Task<Task<TResult>>), inte en enkel Task. Den yttre uppgiften slutförs så snart den asynkrona lambdan når sitt första yield-ögonblick await. Den väntar inte på att den inre uppgiften ska slutföras:

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

Åtgärda problemet på något av tre sätt:

  • Använd Run i stället. Task.Run packar automatiskt upp 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
    
  • Anropa Unwrap för resultatet:

    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
    
  • Vänta två gånger (först den yttre uppgiften, sedan den inre):

    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
    

Saknar 'await' på ett anrop som returnerar en uppgift

Om du anropar en uppgiftsreturmetod i en async metod utan att vänta på den startar metoden den asynkrona åtgärden men väntar inte på att den ska slutföras. Kompilatorn varnar dig om det här fallet med CS4014 i C# och BC42358 i 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

Lagring av resultatet i en variabel undertrycker varningen men åtgärdar inte den underliggande buggen. Alltid await uppgiften såvida du inte avsiktligt vill ha brand-och-glöm-beteende.

Se även