Pembungkus asinkron untuk metode sinkron

Ketika Anda memiliki metode sinkron di pustaka, Anda mungkin tergoda untuk mengekspos versi asinkron yang mengemasnya dalam Task.Run:

public T Foo() { /* synchronous work */ }

// Don't do this in a library:
public Task<T> FooAsync()
{
    return Task.Run(() => Foo());
}

Artikel ini menjelaskan mengapa pendekatan itu hampir selalu salah untuk perpustakaan dan bagaimana mempertimbangkan pertukaran tersebut.

Skalabilitas vs. pemindahan beban

Pemrograman asinkron memberikan dua manfaat yang berbeda:

  • Skalabilitas — Kurangi konsumsi sumber daya dengan membebaskan utas saat menunggu proses I/O.
  • Offloading — Pindahkan pekerjaan ke utas yang berbeda untuk menjaga responsivitas (misalnya, menjaga utas UI tetap bebas) atau mencapai paralelisme.

Manfaat ini membutuhkan pendekatan yang berbeda. Perbedaan kritis: membungkus metode sinkron dalam Task.Run membantu pemindahan beban kerja namun tidak melakukan apa pun untuk skalabilitas.

Mengapa Task.Run tidak meningkatkan skalabilitas

Implementasi yang benar-benar asinkron mengurangi jumlah utas yang digunakan selama operasi yang berjalan lama. Pembungkus Task.Run masih memblokir utas - itu hanya memindahkan pemblokiran dari satu utas ke utas lainnya:

public static class TimerExampleWrong
{
    public static Task SleepAsync(int millisecondsTimeout)
    {
        return Task.Run(() => Thread.Sleep(millisecondsTimeout));
    }
}
Public Module TimerExampleWrong
    Public Function SleepAsync(millisecondsTimeout As Integer) As Task
        Return Task.Run(Sub() Thread.Sleep(millisecondsTimeout))
    End Function
End Module

Bandingkan pendekatan tersebut dengan implementasi yang benar-benar asinkron yang tidak menggunakan utas saat menunggu:

public static class TimerExampleRight
{
    public static Task SleepAsync(int millisecondsTimeout)
    {
        var tcs = new TaskCompletionSource<bool>();
        var timer = new Timer(
            _ => tcs.TrySetResult(true), null, millisecondsTimeout, Timeout.Infinite);

        tcs.Task.ContinueWith(
            _ => timer.Dispose(), TaskScheduler.Default);

        return tcs.Task;
    }
}
Public Module TimerExampleRight
    Public Function SleepAsync(millisecondsTimeout As Integer) As Task
        Dim tcs As New TaskCompletionSource(Of Boolean)()
        Dim tmr As New Timer(
            Sub(state) tcs.TrySetResult(True), Nothing, millisecondsTimeout, Timeout.Infinite)

        tcs.Task.ContinueWith(
            Sub(t) tmr.Dispose(), TaskScheduler.Default)

        Return tcs.Task
    End Function
End Module

Kedua implementasi selesai setelah penundaan yang ditentukan, tetapi implementasi kedua tidak memblokir utas apa pun saat menunggu. Untuk aplikasi server yang menangani banyak permintaan bersamaan, perbedaan itu secara langsung memengaruhi berapa banyak permintaan yang dapat diproses server secara bersamaan.

Offloading adalah tanggung jawab pelanggan

Membungkus panggilan sinkron dalam Task.Run berguna untuk mengalihkan pekerjaan dari utas UI. Namun, konsumen, bukan pustaka, harus menangani pembungkusan ini:

public static class UIOffloadExample
{
    public static int ComputeIntensive(int input)
    {
        int result = 0;
        for (int i = 0; i < input; i++)
        {
            result += i;
        }
        return result;
    }

    public static async Task ConsumeFromUIThreadAsync()
    {
        int result = await Task.Run(() => ComputeIntensive(10_000));
        Console.WriteLine($"Result: {result}");
    }
}
Public Module UIOffloadExample
    Public Function ComputeIntensive(input As Integer) As Integer
        Dim result As Integer = 0
        For i As Integer = 0 To input - 1
            result += i
        Next
        Return result
    End Function

    Public Async Function ConsumeFromUIThreadAsync() As Task
        Dim result As Integer = Await Task.Run(Function() ComputeIntensive(10_000))
        Console.WriteLine($"Result: {result}")
    End Function
End Module

Konsumen memahami konteks mereka: apakah mereka sedang berada pada utas UI, seberapa banyak granularitas yang mereka butuhkan, dan apakah offloading memberikan nilai tambah. Perpustakaan tidak berfungsi.

Mengapa pustaka tidak boleh mengekspos pembungkus asinkron-di-atas-sinkron

Ketika pustaka hanya mengekspos metode sinkron (dan bukan pembungkus asinkron), konsumen mendapat manfaat dalam beberapa cara:

  • Mengurangi area permukaan API: Lebih sedikit metode untuk mempelajari, menguji, dan memelihara.
  • Tidak ada harapan skalabilitas yang menyesatkan: Pengguna tahu bahwa hanya metode yang diekspos sebagai asinkron yang benar-benar memberikan manfaat skalabilitas.
  • Kontrol konsumen: Penelepon memilih apakah dan bagaimana untuk mengalihkan, dengan tingkat detail yang tepat. Aplikasi server throughput tinggi dapat memanggil metode sinkron secara langsung, menghindari overhead yang tidak perlu dari Task.Run.
  • Performa yang lebih baik: Pembungkus asinkron menambahkan overhead melalui alokasi, pergantian konteks, dan penjadwalan thread pool. Untuk operasi dengan tingkat kedetailan tinggi, overhead itu bisa signifikan.

Pengecualian untuk aturan

Beberapa kelas dasar menyediakan metode asinkron sehingga kelas turunan dapat menimpanya dengan implementasi yang benar-benar asinkron. Kelas dasar menyediakan default asinkron-atas-sync.

Misalnya, Stream mengekspos ReadAsync dan WriteAsync. Implementasi dasar membungkus metode sinkron Read dan Write. Kelas turunan seperti FileStream dan NetworkStream mengambil alih metode ini dengan implementasi I/O asinkron yang memberikan manfaat skalabilitas nyata.

Demikian pula, TextReader menyediakan ReadToEndAsync pada kelas dasar sebagai pembungkus, dan StreamReader menimpanya dengan implementasi yang benar-benar asinkron yang memanggil ReadAsync secara internal.

Pengecualian ini valid karena:

  • Pola ini dirancang untuk polimorfisme. Penelepon berinteraksi dengan tipe dasar.
  • Jenis turunan menyediakan penimpaan yang benar-benar asinkron.

Panduan

Mengekspos metode asinkron dari perpustakaan hanya ketika implementasinya menyediakan manfaat skalabilitas yang berarti dibandingkan dengan versi sinkronnya. Jangan mengekspos fungsi asinkron semata-mata untuk pemindahan beban kerja. Serahkan pilihan itu kepada konsumen.

Baca juga