Gyakori aszinkron/várakozási hibák

Az Async/await leegyszerűsíti az aszinkron programozást, de bizonyos hibák ismételten megjelennek. Ez a cikk az aszinkron kód öt leggyakoribb hibáját ismerteti, és bemutatja, hogyan lehet kijavítani őket.

Az Async metódus szinkron módon fut

async A kulcsszó metódushoz való hozzáadása nem teszi lehetővé, hogy a metódus háttérszálon fusson. Arra utasítja a fordítót, hogy engedélyezze await a metódus törzsén belül, és a visszatérési értéket majd csomagolja egy Task-be. Amikor meghív egy aszinkron metódust, az szinkron módon fut, amíg egy nem befejezett várakoztatón el nem éri az első await -t. Ha a metódus nem tartalmaz await kifejezéseket, vagy ha minden várakoztatható objektum, amelyre várakozik, már befejeződött, akkor a metódus teljes egészében a hívó szálon fejeződik be.

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

Itt a metódus azonnal visszaad egy befejezett feladatot, mert soha nem ad eredményül. A fordító figyelmeztetést ad ki, ha egy aszinkron metódus nem tartalmaz await kifejezéseket.

Ha a cél a CPU-kötött munka kiszervezése a szálkészlet egy szálára, használja Run a async helyett.

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

A Task.Run használatának idejére vonatkozó további útmutatásért tekintse meg az Aszinkron burkolókat a szinkron metódusokhoz.

Nem lehet várni az aszinkron void metódust

Ha szinkronizált void-returning metódust aszinkronra konvertál, módosítsa a visszatérési típust a következőre Task: . Ha a visszatérési típust void-ként hagyja meg, a metódus "aszinkron void"-dá válik, amelyre nem lehet várakozni:

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

Az aszinkron üres metódusok egy adott célt szolgálnak: a legfelső szintű eseménykezelők a felhasználói felületi keretrendszerekben. Az eseménykezelőkön kívül mindig térjen vissza Task vagy Task<T> az aszinkron metódusokból. Az aszinkron üres metódusoknak az alábbi hátrányai vannak:

  • A kivételek észrevétlenek maradnak. Az aszinkron void metódusban dobott kivételek arra a környezetre propagálnak, amely aktív volt a metódus indításakor SynchronizationContext . A hívó nem tudja elkapni ezeket a kivételeket.
  • A hívók nem tudják nyomon követni a befejezést. Mechanizmus hiányában nem lehet tudni, mikor fejeződik be a művelet.
  • A tesztelés nehéz. Nem várhatja meg a metódust egy tesztben annak viselkedésének ellenőrzéséhez.

Holtpontok az aszinkron kód blokkolása miatt

Ez a hiba a leggyakoribb oka annak, hogy az aszinkron kód „soha nem fejeződik be”. Ez akkor fordul elő, ha szinkron módon blokkol (hív Wait, Task<TResult>.Result, vagy GetAwaiter.GetResult) egy egyszálas SynchronizationContext szálon.

A holtpontot okozó sorrend:

  1. A felhasználói felületi szál kódja (vagy egy régebbi ASP.NET ASP.NET kérésszála) aszinkron metódust hív meg, és blokkolja a visszaadott feladatot.
  2. Az aszinkron módszer ConfigureAwait(false)nélkül vár egy befejezetlen feladatra.
  3. Amikor a várt tevékenység befejeződik, a folytatás megpróbál visszatérni az eredetihez SynchronizationContext.
  4. Az összefüggés szála blokkolva van, és a feladat befejezésére vár – holtpont.
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

Holtpontok elkerülése

Használjon legalább egy ilyen stratégiát:

  • Ne tiltsa le. Használja a(z) await-t a(z) .Result vagy .Wait() helyett.

    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
    
  • Kódtárkódban használható ConfigureAwait(false) . Ha a könyvtár metódusának nem kell folytatnia a hívó környezetében, adja meg ConfigureAwait(false) minden 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
    

    A ConfigureAwait(false) használata jelzi a futtatókörnyezetnek, hogy ne küldje vissza a folytatást az eredeti SynchronizationContext-re. Ez a megközelítés védelmet nyújt a letiltó hívóknak, és a szükségtelen szálugraások elkerülésével javítja a teljesítményt.

Figyelmeztetés

Statikus konstruktor holtpont. A CLR statikus konstruktorok (cctorek) futtatása közben zárolást tartalmaz. Ha egy statikus konstruktor blokkol egy tevékenységet, és a tevékenység folytatásának kódot kell futtatnia ugyanabban a típusban (vagy az építési láncban részt vevő típusban), a folytatás nem folytatható, mert a cctor zárolást megtartotta. Kerülje a hívások blokkolását a statikus konstruktorokban teljes mértékben.

Tevékenység<Tevékenység> kibontása

Ha egy aszinkron lambdát ad át egy olyan metódusnak, mint a StartNew, a visszaadott objektum egy Task<Task> (vagy Task<Task<TResult>>), nem egyszerű Task. A külső feladat akkor fejeződik be, amikor az aszinkron lambda eléri első hozamát await. Nem várja meg, amíg a belső feladat befejeződik:

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

A probléma megoldása háromféleképpen lehetséges:

  • A Run használható helyette. Task.Run automatikusan kibontja 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
    
  • Az eredmény meghívása 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
    
  • Várjon kétszer (először a külső feladat, majd a belső):

    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
    

Hiányzó 'await' egy feladatot visszaadó hívásnál

Ha egy async metódusban feladat-visszaadó metódust hív meg anélkül, hogy megvárná, a metódus elindítja az aszinkron műveletet, de nem várja meg, hogy befejeződjön. A fordító figyelmezteti önt erre az esetre a CS4014 C# és BC42358 Visual Basic esetén:

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

Az eredmény változóban való tárolása letiltja a figyelmeztetést, de nem javítja ki a mögöttes hibát. Mindig await hajtsa végre a feladatot, hacsak nem szándékosan szeretne "fire-and-forget" módot alkalmazni.

Lásd még