Yaygın Asenkron/await hataları

Async/await, zaman uyumsuz programlamayı basitleştirir, fakat aynı hatalar tekrarlanıyor. Bu makalede, zaman uyumsuz koddaki en yaygın beş hata açıklanır ve her birinin nasıl düzeltileceğini gösterir.

Asenkron yöntem senkron olarak çalışır

anahtar sözcüğünü async bir yönteme eklemek, yöntemin arka plan iş parçacığında çalıştırılmasını sağlamaz. Derleyiciye, yöntem gövdesinde await'e izin vermesini ve dönüş değerini bir Task içinde sarmalamasını söyler. Eşzamanlı olmayan bir yöntemi çağırdığınızda, tamamlanmamış bir beklenebilir nesnede ilk await öğesine ulaşana kadar eşzamanlı olarak çalışır. Yöntem, hiçbir await ifadesi içermiyorsa veya her beklediği beklenebilir zaten tamamlanmışsa, yöntem tamamen çağrı yapan iş parçacığında tamamlanır.

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

Burada yöntemi, hiçbir zaman verim vermediğinden hemen tamamlanmış bir görev döndürür. Async bir metodda await ifadeleri eksik olduğunda derleyici bir uyarı verir.

Eğer amacınız CPU'ya bağlı çalışmayı bir iş parçacığı havuzu iş parçacığına devretmekse, Run yerine async kullanın.

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

Task.Run ne zaman kullanılacağını öğrenmek için, Zaman Uyumlu Yöntemler için Zaman Uyumsuz Sarmalayıcılar başlıklı makaleye bakın.

Zaman uyumsuz bir void yöntemi beklenemez

Senkron void dönen bir yöntemi asenkron hale çevirdiğinizde, dönüş türünü Task olarak değiştirin. Dönüş türünü void olarak bırakırsanız, yöntem "async void" olur ve bunu bekleyemezsiniz:

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 yöntemler belirli bir amaca hizmet eder: UI çerçevelerinde üst düzey olay işleyicileri. Olay işleyicileri dışında, zaman uyumsuz yöntemlerden her zaman Task veya Task<T> döndürün. Asenkron void yöntemlerinin şu dezavantajları vardır:

  • İstisnalar göz ardı edilir. Eşzamanlı olmayan bir void metot içinde fırlatılan istisnalar, metot başladığında etkin olan bağlama yayılır SynchronizationContext. Çağıran bu istisnaları yakalayamıyor.
  • Arayanlar tamamlanmayı takip edemiyor. Task olmadan işlemin ne zaman tamamlandığını bilmek için bir mekanizma yoktur.
  • Test zordur. Bir testte yönteminin davranışını doğrulamasını bekleyemazsınız.

Zaman uyumsuz kodda bloklamadan kaynaklanan kilitlenmeler

Bu hata, "hiç tamamlanmayan" asenkron kodun en yaygın nedenidir. Tek iş parçacıklı bir Wait iş parçacığı üzerinde senkron olarak engellediğinizde (Task<TResult>.Result, GetAwaiter veya GetResult.SynchronizationContext çağrısı) gerçekleşir.

Kilitlenmeye neden olan dizi:

  1. tr-TR: UI iş parçacığındaki kod (veya eski ASP.NET'te bir ASP.NET istek iş parçacığında) zaman uyumsuz bir yöntemi çağırır ve döndürülen görev üzerinde bekler.
  2. eşzamanlı olmayan yöntem, ConfigureAwait(false) kullanmadan tamamlanmamış bir görevi bekliyor.
  3. Beklenen görev tamamlandığında, devam özgün SynchronizationContext öğesine gönderme yapmaya çalışır.
  4. O bağlamın iş parçacığı, görevin tamamlanmasını beklerken engellenir ve tıkanma meydana gelir.
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

Kilitlenmeleri önleme

Şu stratejilerden birini veya daha fazlasını kullanın:

  • Engelleme. veya awaityerine .Result kullanın.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
    
  • Kütüphane kodunda ConfigureAwait(false) kullanın. Kitaplık yönteminizin çağıranın bağlamında sürdürülmesi gerekmediğinde, her ConfigureAwait(false)üzerinde belirtinawait:

    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
    

    ConfigureAwait(false) kullanılarak çalışma zamanı, devamlılığı özgün SynchronizationContext'e geri göndermemesi gerektiği belirtilir. Bu yaklaşım, bloklayan arayanları korur ve gereksiz iş parçacığı geçişlerinden kaçınarak performansı artırır.

Uyarı

Statik oluşturucu kilitlenmeleri. CLR, statik oluşturucuları (cctorlar) çalıştırırken kilit tutar. Statik bir oluşturucu bir görev üzerinde blok oluşturuyorsa ve bu görevin devamı aynı türde (veya yapı zincirinde yer alan bir tür) kod çalıştırmaya ihtiyaç duyuyorsa, kilit tutulacağından cctor devam edilemiyordur. Statik oluşturucuların içindeki çağrıları tamamen engellemekten kaçının.

Görev<Görev> açma

Bir zaman uyumsuz lambda'yı StartNew gibi bir yönteme geçtiğinizde, döndürülen nesne basit bir Task<Task> değil, bir Task<Task<TResult>> (veya Task) olur. Dış görev, zaman uyumsuz lambda ilk verimine ulaştığında tamamlar await. İç görevin bitmesini beklemez:

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

Bu sorunu üç yoldan biriyle düzeltin:

  • Bunun yerine Run kullanın. Task.Run otomatik olarak çözer 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
    
  • Sonuç üzerinde çağrısı 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
    
  • İki kez bekle (önce dış görev, sonra iç):

    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
    

Görev döndüren bir çağrıda 'await' anahtar kelimesi eksik

Bir async yönteminde görev döndüren bir yöntemi beklemeden çağırırsanız, yöntem zaman uyumsuz işlemi başlatır ancak tamamlanmasını beklemez. Derleyici, C# dilinde CS4014 ve Visual Basic'da BC42358 ile bu durum hakkında sizi uyarır:

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

Sonucun bir değişkende depolanması uyarıyı bastırır ancak temel alınan hatayı düzeltmez. Eğer kasıtlı olarak ateşle ve unutma davranışı istemiyorsanız, her zaman await görevi gerçekleştirin.

Ayrıca bakınız