Eşzamansız yöntemler için eşzamanlı sarmalayıcılar

Bir kütüphane yalnızca asenkron API'leri kullanıma sunarken, tüketiciler bazen zaman uyumlu bir arabirime veya sözleşmeye uymak için bunları zaman uyumlu çağrılarda sarmalar. Bu "sync-over-async" deseni basit görünebilir, ancak kilitlenmelere ve performans sorunlarına sıkça neden olur.

Temel sarmalama desenleri

Görev Tabanlı Zaman Uyumsuz Desen (TAP) yönteminin etrafındaki zaman uyumlu sarmalayıcı, görevin Result özelliğine erişir ve bu da çağıran iş parçacığını engeller:

public class TapWrapper
{
    public static int Foo(Func<Task<int>> fooAsync)
    {
        return fooAsync().Result;
    }
}
Public Module TapWrapper
    Public Function Foo(fooAsync As Func(Of Task(Of Integer))) As Integer
        Return fooAsync().Result
    End Function
End Module

Bu yaklaşım basit görünür, ancak çalıştığı ortama bağlı olarak ciddi sorunlara neden olabilir.

Tek iş parçacıklı bağlamlarda kilitlenmeler

En tehlikeli senaryo, tek iş parçacıklı bir SynchronizationContext iş parçacığından zaman uyumlu bir sarmalayıcı çağırdığınızda oluşur. Bu senaryo genellikle WPF, Windows Forms veya MAUI uygulamalarında bir kullanıcı arabirimi iş parçacığıdır.

public static class DeadlockExample
{
    private static void Delay(int milliseconds)
    {
        DelayAsync(milliseconds).Wait();
    }

    private static async Task DelayAsync(int milliseconds)
    {
        await Task.Delay(milliseconds);
    }
}
Public Module DeadlockExample
    Private Sub Delay(milliseconds As Integer)
        DelayAsync(milliseconds).Wait()
    End Sub

    Private Async Function DelayAsync(milliseconds As Integer) As Task
        Await Task.Delay(milliseconds)
    End Function
End Module

Adım adım şöyle olur:

  1. Kullanıcı arabirimi iş parçacığı Delay'ı çağırır ve bu da DelayAsync(milliseconds).Wait()'i çağırır.
  2. DelayAsync, await Task.Delay(milliseconds) öğesine ulaşana kadar eşzamanlı olarak çalışır.
  3. Gecikme henüz tamamlanmadığından geçerli awaitSynchronizationContext değeri yakalar ve askıya alır. DelayAsync çağırana bir Task döndürür.
  4. kullanıcı arabirimi iş parçacığı, .Wait()içinde bu görevin tamamlanmasını bekler.
  5. Gecikme tamamlandığında, devamın kullanıcı arabirimi iş parçacığı olan özgün SynchronizationContext üzerinde çalıştırılması gerekir.
  6. Kullanıcı arabirimi iş parçacığı .Wait() nedeniyle bloklandığı için devam işlemini gerçekleştiremiyor.
  7. Kilitlenme.

Önemli

Eşzamansız üzerinde eşzamanlı kodun başarısı veya başarısızlığı, çalıştığı ortama bağlıdır. Konsol uygulamasında çalışan kod, kullanıcı arabirimi iş parçacığında veya ASP.NET 'de (.NET Framework'te) kilitlenmeye neden olabilir. Bu ortam bağımlılığı, zaman uyumlu sarmalayıcıları ortaya çıkarmaktan kaçınmanın temel bir nedenidir.

İş parçacığı havuzu tükenmesi

Kilitlenmeler kullanıcı arabirimi iş parçacıklarıyla sınırlı değildir. Eşzamanlı olmayan bir yöntem, çalışmasını tamamlamak için iş parçacığı havuzuna dayanıyorsa, örneğin son işleme adımını sıraya koyarak, senkron sarıcılar ile havuzdaki birçok iş parçacığını engellemek havuzu aç bırakabilir.

public static class ThreadPoolDeadlockExample
{
    public static int Foo(Func<Task<int>> fooAsync)
    {
        return fooAsync().Result;
    }

    public static async Task DemonstrateDeadlockRiskAsync()
    {
        var tasks = Enumerable.Range(0, 25)
            .Select(_ => Task.Run(() => Foo(() => SomeIOOperationAsync())));
        await Task.WhenAll(tasks);
    }

    private static async Task<int> SomeIOOperationAsync()
    {
        await Task.Delay(100);
        return 42;
    }
}

Bu senaryoda:

  1. Birçok iş parçacığı havuzu iş parçacıkları Foo'yi çağırarak .Result içinde bloke eder.
  2. Her bir uyumsuz işlem G/Ç'sini tamamlar ve tamamlama geri çağırmasını çalıştırmak için bir iş parçacığı havuzundaki bir iş parçacığına ihtiyaç duyar.
  3. Engellenen çağrılar kullanılabilir çalışan iş parçacıklarını kapladığı için, bir iş parçacığının kullanılabilir duruma gelmesi için tamamlanmaların uzun süre beklemesi gerekebilir.
  4. Modern .NET zaman içinde iş parçacığı havuzuna daha fazla iş parçacığı ekleyebilir, ancak uygulama yine de ciddi iş parçacığı havuzu yetersizliği, düşük aktarım hızı, uzun gecikmeler veya belirgin bir kilitlenme yaşayabilir.

Bu desen, .NET Framework 1.x'te, zaman uyumlu yöntemin zaman uyumsuz BeginGetResponse/EndGetResponse çevresinde sarmalayıcı olarak uygulandığı durumu etkilemiştir.

Kılavuz: Zaman uyumlu sarmalayıcıları açığa çıkarmaktan kaçının

Zaman uyumsuz bir uygulamayı sarmalayan zaman uyumlu bir yöntemi açığa çıkarmayın. Bunun yerine, engelleme kararını tüketiciye bırakın. Tüketici iş parçacığı ortamını bilir ve bilinçli bir seçim yapabilir.

Zaman uyumsuz bir yöntemi zaman uyumlu olarak çağırmanız gerektiğini fark ederseniz, önce kodu "tamamen zaman uyumsuz" olacak şekilde yeniden yapılandırıp yapılandıramayacağınızı düşünün. Yeniden düzenleme genellikle daha iyi uzun vadeli bir çözümdür.

Senkron-üzerinden-asenkron kaçınılmaz olduğunda hafifletme stratejileri

Bazen zaman uyumsuz eşitleme gerçekten kaçınılmazdır. Örneğin, zaman uyumlu bir yöntem gerektiren bir arabirim uyguladığınızda ve tek mevcut uygulama zaman uyumsuz olduğunda bu durum kaçınılmazdır. Bu gibi durumlarda, riski azaltmak için aşağıdaki stratejileri uygulayın.

Eşzamanlı olmayan uygulamada ConfigureAwait(false) kullan

Eğer asenkron yöntemi kontrol ediyorsanız, devamın orijinal SynchronizationContext'e geri taşınmasını önlemek için her await ile Task.ConfigureAwaitfalse kullanın.

public static class ConfigureAwaitMitigation
{
    public static async Task<int> LibraryMethodAsync()
    {
        await Task.Delay(100).ConfigureAwait(false);
        return 42;
    }

    public static int Sync()
    {
        return LibraryMethodAsync().GetAwaiter().GetResult();
    }
}
Public Module ConfigureAwaitMitigation
    Public Async Function LibraryMethodAsync() As Task(Of Integer)
        Await Task.Delay(100).ConfigureAwait(False)
        Return 42
    End Function

    Public Function Sync() As Integer
        Return LibraryMethodAsync().Result
    End Function
End Module

Bir kitaplık yazarı olarak, kodunuzun yakalanan bağlamda devam etmesi gerekmediği sürece tüm await ifadelerinde ConfigureAwait(false) kullanın. Kullanmak ConfigureAwait(false) , performans için en iyi yöntemdir ve tüketiciler engellediğinde kilitlenmeleri önlemeye yardımcı olur.

İş parçacığı havuzuna boşaltma

Zaman uyumsuz uygulamayı denetlemezseniz (ve ConfigureAwait(false) kullanmıyor olabilir), çağrıyı iş parçacığı havuzuna aktarın. İş parçacığı havuzunda bir SynchronizationContext yok, bu nedenle await engellenen bir iş parçacığına yeniden taşımayı denemez:

public int Sync()
{
    return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
    Return Task.Run(Function() Library.FooAsync()).Result
End Function

Birden çok ortamda test

Zaman uyumlu bir sarmalayıcı göndermeniz gerekiyorsa, şuradan test edin:

  • Kullanıcı arabirimi (UI) iş parçacığı (WPF, Windows Forms).
  • Yük altında olan iş parçacığı havuzu.
  • En yüksek iş parçacığı sayısı düşük olan iş parçacığı havuzu.
  • Konsol uygulaması.

Bir ortamda çalışan davranış başka bir ortamda kilitlenmeye neden olabilir.

Ayrıca bakınız