Pembungkus sinkron untuk metode asinkron

Ketika pustaka hanya mengekspos API asinkron, konsumen terkadang membungkusnya dalam panggilan sinkron untuk memenuhi antarmuka atau kontrak yang sinkron. Pola "sync-over-async" ini bisa tampak mudah, tetapi merupakan sumber umum kebuntuan dan masalah kinerja.

Pola pembungkusan dasar

Pembungkus sinkron di sekitar metode Pola Asinkron Berbasis Tugas (TAP) mengakses properti tugas Result , yang memblokir utas panggilan:

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

Pendekatan ini terlihat sederhana, tetapi dapat menyebabkan masalah serius tergantung pada lingkungan tempatnya berjalan.

Kebuntuan dengan konteks berutas tunggal

Skenario paling berbahaya terjadi ketika Anda memanggil pembungkus sinkron dari utas yang memiliki SynchronizationContext utas tunggal. Skenario ini biasanya merupakan utas UI di aplikasi WPF, Formulir Windows, atau MAUI.

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

Inilah yang terjadi langkah demi langkah:

  1. Utas Delay memanggil UI, yang kemudian memanggil DelayAsync(milliseconds).Wait().
  2. DelayAsync berjalan secara sinkron sampai mencapai await Task.Delay(milliseconds).
  3. Karena penundaan belum selesai, await menangkap SynchronizationContext saat ini dan menghentikan sementara. DelayAsync mengembalikan Task ke pemanggilnya.
  4. Utas UI diblokir di .Wait(), menunggu hingga tugas tersebut selesai.
  5. Ketika penundaan selesai, kelanjutan perlu berjalan pada aslinya SynchronizationContext yang merupakan utas UI.
  6. Utas UI tidak dapat memproses kelanjutan karena diblokir di .Wait().
  7. Kehadangan.

Penting

Keberhasilan atau kegagalan kode sync-over-async tergantung pada lingkungan di mana ia dijalankan. Kode yang berfungsi di aplikasi konsol mungkin mengalami kebuntuan pada utas UI atau di ASP.NET (di .NET Framework). Dependensi lingkungan ini adalah alasan inti untuk menghindari mengekspos pembungkus sinkron.

Kehabisan kolam utas

Kebuntuan tidak terbatas pada utas UI. Jika metode asinkron tergantung pada kumpulan utas untuk menyelesaikan pekerjaannya, misalnya, dengan mengantre langkah pemrosesan akhir, memblokir banyak rangkaian kumpulan dengan pembungkus sinkron dapat membuat kumpulan kelaparan:

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;
    }
}

Dalam skenario ini:

  1. Banyak utas kumpulan utas memanggil Foo, yang memblokir di .Result.
  2. Setiap operasi asinkron menyelesaikan I/O-nya dan memerlukan thread dari kumpulan thread untuk menjalankan callback penyelesaian.
  3. Karena panggilan yang diblokir menempati utas pekerja yang tersedia, penyelesaian mungkin harus menunggu lama sampai sebuah utas menjadi tersedia.
  4. .NET modern dapat menambahkan lebih banyak utas kumpulan utas dari waktu ke waktu, tetapi aplikasi masih dapat mengalami kelaparan kumpulan utas yang parah, throughput buruk, penundaan panjang, atau hang yang jelas.

Pola ini mempengaruhi HttpWebRequest.GetResponse dalam .NET Framework 1.x, di mana metode sinkron diimplementasikan sebagai pembungkus untuk metode asinkron BeginGetResponse/EndGetResponse.

Pedoman: Hindari mengekspos pembungkus sinkron

Jangan mengekspos metode sinkron yang membungkus implementasi asinkron. Sebaliknya, tinggalkan keputusan apakah akan memblokir kepada konsumen. Pengguna mengetahui lingkungan threading mereka dan dapat membuat pilihan yang terinformasi.

Jika Anda merasa perlu memanggil metode asinkron secara sinkron, pertimbangkan terlebih dahulu apakah Anda dapat merestrukturisasi kode menjadi "asinkron sepanjang jalan ke bawah." Pemfaktoran ulang sering kali merupakan solusi jangka panjang yang lebih baik.

Strategi mitigasi saat sync-over-async tidak dapat dihindari

Terkadang sync-over-async benar-benar tidak dapat ditolak. Misalnya, itu tidak dapat ditolak ketika Anda menerapkan antarmuka yang memerlukan metode sinkron, dan satu-satunya implementasi yang tersedia adalah asinkron. Dalam kasus tersebut, terapkan strategi berikut untuk mengurangi risiko.

Gunakan ConfigureAwait(false) dalam implementasi asinkron

Jika Anda mengontrol metode asinkron, gunakan Task.ConfigureAwait dengan false pada setiap await untuk mencegah kelanjutan dari marshaling kembali ke SynchronizationContext yang asli.

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

Sebagai penulis pustaka, gunakan ConfigureAwait(false) pada semua await kecuali jika kode Anda secara khusus perlu dilanjutkan dalam konteks yang sedang aktif. Menggunakan ConfigureAwait(false) adalah praktik terbaik untuk performa dan membantu mencegah kebuntuan saat konsumen memblokir.

Offload ke kolam thread

Jika Anda tidak mengontrol implementasi asinkron (dan mungkin tidak menggunakan ConfigureAwait(false)), pindahkan panggilan ke kumpulan utas. Kumpulan utas tidak memiliki SynchronizationContext, sehingga menunggu tidak akan mencoba untuk mengalokasikan kembali kontrol ke utas yang diblokir.

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

Menguji di beberapa lingkungan

Jika Anda harus mengirim pembungkus sinkron, uji dari:

  • Utas UI (WPF, Formulir Windows).
  • Kolam utas sedang dalam kondisi beban.
  • Kumpulan utas dengan jumlah utas maksimum rendah.
  • Aplikasi konsol.

Perilaku yang berfungsi di satu lingkungan mungkin mengalami deadlock di lingkungan lain.

Baca juga