Menerapkan pola asinkron berbasis tugas

Anda dapat menerapkan pola asinkron berbasis tugas (TAP) dengan tiga cara: dengan menggunakan pengompilasi C# dan Visual Basic di Visual Studio, secara manual, atau melalui kombinasi metode kompilator dan manual. Bagian berikut membahas setiap metode secara rinci. Anda dapat menggunakan pola TAP untuk mengimplementasikan operasi asinkron yang terikat pada komputasi maupun terikat pada I/O. Bagian Beban Kerja membahas setiap jenis operasi.

Membuat metode TAP

Menggunakan pengkompilasi

Dimulai dengan .NET Framework 4.5, metode apa pun yang dikaitkan dengan kata kunci async (Async dalam Visual Basic) dianggap sebagai metode asinkron. Pengompilasi C# dan Visual Basic melakukan transformasi yang diperlukan untuk mengimplementasikan metode secara asinkron dengan menggunakan TAP. Metode asinkron harus mengembalikan System.Threading.Tasks.Task objek atau System.Threading.Tasks.Task<TResult> . Untuk yang terakhir, isi fungsi harus mengembalikan TResult, dan pengkompilasi memastikan bahwa hasil ini tersedia melalui objek tugas yang dihasilkan. Demikian pula, setiap pengecualian yang tidak tertangani dalam isi metode diteruskan ke tugas output dan menyebabkan tugas yang dihasilkan berakhir dalam status TaskStatus.Faulted. Pengecualian untuk aturan ini adalah ketika OperationCanceledException (atau jenis turunan) tidak tertangani, dalam hal ini tugas yang dihasilkan berakhir dalam TaskStatus.Canceled status.

Task.Start dan penghapusan tugas

Gunakan Start hanya untuk tugas yang dibuat secara eksplisit dengan Task konstruktor yang masih dalam status Created . Metode TAP publik harus mengembalikan tugas aktif, sehingga penelepon tidak perlu memanggil Start.

Di sebagian besar kode TAP, jangan hapus tugas. Task Tidak menyimpan sumber daya yang tidak dikelola dalam kasus umum, dan membuang setiap tugas menambahkan overhead tanpa manfaat praktis. Buang hanya ketika API atau pengukuran tertentu menunjukkan kebutuhan.

Jika Anda memulai pekerjaan latar belakang yang bertahan melampaui jalur panggilan langsung, pastikan kepemilikan tetap jelas dan pantau penyelesaiannya. Untuk panduan selengkapnya, lihat Menjaga metode asinkron tetap hidup.

Membuat metode TAP secara manual

Anda dapat menerapkan pola TAP secara manual untuk kontrol yang lebih baik atas implementasi. Pengkompilasi bergantung pada area antarmuka publik yang diekspos dari System.Threading.Tasks namespace dan jenis pendukung di System.Runtime.CompilerServices namespace. Untuk mengimplementasikan TAP sendiri, Anda membuat TaskCompletionSource<TResult> objek, melakukan operasi asinkron, dan ketika selesai, panggil SetResultmetode , SetException, atau SetCanceled , atau Try versi salah satu metode ini. Saat menerapkan metode TAP secara manual, Anda harus menyelesaikan tugas yang dihasilkan saat operasi asinkron yang diwakili selesai. Contohnya:

static class StreamExtensions
{
    public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object? state)
    {
        var tcs = new TaskCompletionSource<int>();
        stream.BeginRead(buffer, offset, count, ar =>
        {
            try { tcs.SetResult(stream.EndRead(ar)); }
            catch (Exception exc) { tcs.SetException(exc); }
        }, state);
        return tcs.Task;
    }
}
Module StreamExtensions
    <Extension()>
    Public Function ReadTask(stream As Stream, buffer As Byte(),
                             offset As Integer, count As Integer,
                             state As Object) As Task(Of Integer)
        Dim tcs As New TaskCompletionSource(Of Integer)()
        stream.BeginRead(buffer, offset, count,
            Sub(ar)
                Try
                    tcs.SetResult(stream.EndRead(ar))
                Catch exc As Exception
                    tcs.SetException(exc)
                End Try
            End Sub, state)
        Return tcs.Task
    End Function
End Module

Pendekatan hibrid

Anda mungkin merasa berguna untuk menerapkan pola TAP secara manual tetapi mendelegasikan logika inti untuk implementasi ke pengkompilasi. Misalnya, Anda mungkin ingin menggunakan pendekatan hibrid ketika Anda ingin memverifikasi argumen di luar metode asinkron yang dihasilkan kompilator sehingga pengecualian dapat lolos ke pemanggil langsung metode daripada diekspos melalui System.Threading.Tasks.Task objek:

class Calculator
{
    private int value = 0;

    public Task<int> MethodAsync(string input)
    {
        if (input == null) throw new ArgumentNullException(nameof(input));
        return MethodAsyncInternal(input);
    }

    private async Task<int> MethodAsyncInternal(string input)
    {
        // code that uses await goes here
        await Task.Delay(1);
        return value;
    }
}
Class Calculator
    Private value As Integer = 0

    Public Function MethodAsync(input As String) As Task(Of Integer)
        If input Is Nothing Then Throw New ArgumentNullException(NameOf(input))
        Return MethodAsyncInternal(input)
    End Function

    Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
        ' code that uses await goes here
        Await Task.Delay(1)
        Return value
    End Function
End Class

Kasus lain di mana delegasi tersebut berguna adalah ketika Anda menerapkan pengoptimalan jalur cepat dan ingin mengembalikan tugas yang di-cache.

Beban Kerja

Anda dapat menerapkan operasi asinkron yang terikat pada komputasi maupun I/O sebagai metode TAP. Namun, saat Anda mempublikasikan metode TAP dari pustaka, gunakan hanya untuk beban kerja yang melibatkan operasi yang terkait I/O. Operasi ini mungkin juga melibatkan komputasi, tetapi seharusnya tidak murni komputasi. Jika metode sepenuhnya bergantung pada komputasi, tampilkan hanya sebagai implementasi sinkron. Kode yang mengonsumsinya kemudian dapat memilih apakah akan membungkus pemanggilan metode sinkron tersebut ke dalam sebuah task untuk memindahkan pekerjaan ke utas lain atau demi mencapai paralelisme. Jika metode terikat I/O, ekspos hanya sebagai implementasi asinkron.

Tugas yang terbatas oleh komputasi

Kelas System.Threading.Tasks.Task ini bekerja dengan baik untuk mewakili operasi intensif komputasi. Secara bawaan, dapat memanfaatkan dukungan khusus dalam kelas ThreadPool untuk memberikan eksekusi yang efisien. Ini juga memberikan kontrol yang signifikan atas kapan, di mana, dan bagaimana komputasi asinkron dijalankan.

Hasilkan tugas terikat komputasi dengan cara berikut:

  • Di .NET Framework 4.5 dan versi yang lebih baru (termasuk .NET Core dan .NET 5+), gunakan metode statis Task.Run sebagai pintasan ke TaskFactory.StartNew. Gunakan Run untuk dengan mudah meluncurkan tugas yang terikat oleh komputasi yang menargetkan kumpulan utas. Metode ini adalah mekanisme yang disukai untuk meluncurkan tugas yang terikat komputasi. Gunakan StartNew secara langsung hanya ketika Anda menginginkan kontrol yang lebih halus atas tugas.

  • Di .NET Framework 4, gunakan metode TaskFactory.StartNew. Ini menerima sebuah delegasi (biasanya sebuah Action<T> atau Func<TResult>) untuk mengeksekusi tugas secara asinkron. Jika Anda menyediakan Action<T> delegasi, metode tersebut mengembalikan System.Threading.Tasks.Task objek yang mewakili eksekusi asinkron dari delegasi tersebut. Jika Anda memberikan Func<TResult> delegasi, metode mengembalikan System.Threading.Tasks.Task<TResult> objek. Kelebihan beban StartNew metode menerima token pembatalan (CancellationToken), opsi pembuatan tugas (TaskCreationOptions), dan penjadwal tugas (TaskScheduler). Parameter ini memberikan kontrol terperinci atas penjadwalan dan eksekusi tugas. Instans pabrik yang menargetkan penjadwal tugas saat ini tersedia sebagai properti statis (Factory) kelas Task . Misalnya: Task.Factory.StartNew(…).

  • Gunakan konstruktor jenis Task dan Start metode jika Anda ingin membuat dan menjadwalkan tugas secara terpisah. Metode publik hanya boleh mengembalikan tugas yang sudah dimulai.

  • Gunakan kelebihan beban Task.ContinueWith metode. Metode ini membuat tugas baru yang dijadwalkan ketika tugas lain selesai. ContinueWith Beberapa kelebihan beban menerima token pembatalan, opsi kelanjutan, dan penjadwal tugas untuk kontrol yang lebih baik atas penjadwalan dan eksekusi tugas kelanjutan.

  • Gunakan metode TaskFactory.ContinueWhenAll dan TaskFactory.ContinueWhenAny. Metode ini membuat tugas baru yang dijadwalkan ketika semua atau salah satu dari sekumpulan tugas yang disediakan selesai. Metode ini juga menyediakan kelebihan beban untuk mengontrol penjadwalan dan eksekusi tugas-tugas ini.

Dalam tugas yang terikat komputasi, sistem dapat mencegah eksekusi tugas terjadwal jika menerima permintaan pembatalan sebelum mulai menjalankan tugas. Dengan demikian, jika Anda memberikan token pembatalan (CancellationToken objek), Anda dapat meneruskan token tersebut ke kode asinkron yang memantau token. Anda juga dapat memberikan token ke salah satu metode yang disebutkan sebelumnya seperti StartNew atau Run sehingga Task runtime juga dapat memantau token.

Misalnya, pertimbangkan metode asinkron yang merender gambar. Isi tugas dapat memeriksa secara berkala token pembatalan sehingga kode berhenti lebih awal jika permintaan pembatalan tiba selama pemrosesan. Selain itu, jika permintaan pembatalan tiba sebelum penyajian dimulai, Anda ingin mencegah operasi penyajian:

internal static Task<Bitmap> RenderAsync(ImageData data, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        var bmp = new Bitmap(data.Width, data.Height);
        for (int y = 0; y < data.Height; y++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            for (int x = 0; x < data.Width; x++)
            {
                // render pixel [x,y] into bmp
            }
        }
        return bmp;
    }, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As CancellationToken) As Task(Of Bitmap)
    Return Task.Run(Function()
                        Dim bmp As New Bitmap(data.Width, data.Height)
                        For y As Integer = 0 To data.Height - 1
                            cancellationToken.ThrowIfCancellationRequested()
                            For x As Integer = 0 To data.Width - 1
                                ' render pixel [x,y] into bmp
                            Next
                        Next
                        Return bmp
                    End Function, cancellationToken)
End Function

Note

Sampel ini menggunakan Bitmap, yang memerlukan paket System.Drawing.Common dan hanya didukung di Windows. Pola tugas terikat komputasi—menggunakan Task.Run dengan CancellationToken—berlaku di semua platform; ganti pustaka pencitraan lintas platform untuk target non-Windows.

Tugas terikat komputasi berakhir dalam status Canceled jika setidaknya salah satu kondisi berikut ini benar:

  • Permintaan pembatalan tiba melalui CancellationToken objek, yang disediakan sebagai argumen untuk metode pembuatan (misalnya, StartNew atau Run) sebelum tugas beralih ke Running status.

  • Pengecualian OperationCanceledException tidak tertangani dalam isi tugas seperti itu. Pengecualian tersebut berisi elemen yang sama CancellationToken yang diteruskan ke tugas, dan token tersebut menunjukkan bahwa pembatalan diminta.

Jika pengecualian lain tidak tertangani dalam isi tugas, tugas berakhir dalam status Faulted . Setiap upaya untuk menunggu tugas atau mengakses hasilnya akan memunculkan pengecualian.

Tugas yang bergantung pada I/O

Untuk membuat tugas yang tidak perlu langsung menggunakan utas untuk seluruh eksekusi, gunakan tipe TaskCompletionSource<TResult>. Jenis ini mengekspos properti Task yang mengembalikan instans Task<TResult> yang terkait. Anda mengontrol siklus hidup tugas ini dengan menggunakan metode seperti TaskCompletionSource<TResult>, , SetResultSetException, dan variannyaSetCanceled.TrySet

Misalkan Anda ingin membuat tugas yang selesai setelah periode waktu tertentu. Misalnya, Anda mungkin ingin menunda aktivitas di antarmuka pengguna. Kelas System.Threading.Timer sudah menyediakan kemampuan untuk secara asinkron memanggil delegasi setelah periode waktu yang ditentukan. Dengan menggunakan TaskCompletionSource<TResult>, Anda dapat meletakkan Task<TResult> front pada timer. Contohnya:

public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
    TaskCompletionSource<DateTimeOffset>? tcs = null;
    Timer? timer = null;

    timer = new Timer(delegate
    {
        timer!.Dispose();
        tcs!.TrySetResult(DateTimeOffset.UtcNow);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<DateTimeOffset>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
    Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(DateTimeOffset.UtcNow)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

Metode Task.Delay ini disediakan untuk tujuan ini. Anda dapat menggunakannya di dalam metode asinkron lain, misalnya, untuk mengimplementasikan perulangan polling asinkron:

public static async Task Poll(Uri url, CancellationToken cancellationToken, IProgress<bool> progress)
{
    while (true)
    {
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
        bool success = false;
        try
        {
            await DownloadStringAsync(url);
            success = true;
        }
        catch { /* ignore errors */ }
        progress.Report(success);
    }
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
                           progress As IProgress(Of Boolean)) As Task
    Do While True
        Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
        Dim success As Boolean = False
        Try
            Await DownloadStringAsync(url)
            success = True
        Catch
            ' ignore errors
        End Try
        progress.Report(success)
    Loop
End Function

Kelas TaskCompletionSource<TResult> tidak memiliki rekan non-generik. Namun, Task<TResult> berasal dari Task, sehingga Anda dapat menggunakan objek generik TaskCompletionSource<TResult> untuk metode terikat I/O yang hanya mengembalikan tugas. Untuk melakukan ini, gunakan sumber dengan dummy TResult (Boolean adalah pilihan default yang baik, tetapi jika Anda khawatir pengguna Task melakukan downcasting ke Task<TResult>, Anda dapat menggunakan tipe privat TResult sebagai gantinya). Misalnya, Delay metode dalam contoh sebelumnya mengembalikan waktu saat ini bersama dengan offset yang dihasilkan (Task<DateTimeOffset>). Jika nilai hasil seperti itu tidak perlu, metode tersebut dapat dikodekan sebagai berikut (perhatikan perubahan jenis pengembalian dan perubahan argumen menjadi TrySetResult):

public static Task<bool> DelaySimple(int millisecondsTimeout)
{
    TaskCompletionSource<bool>? tcs = null;
    Timer? timer = null;

    timer = new Timer(delegate
    {
        timer!.Dispose();
        tcs!.TrySetResult(true);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<bool>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function DelaySimple(millisecondsTimeout As Integer) As Task(Of Boolean)
    Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(True)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of Boolean)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

Tugas yang memerlukan kombinasi komputasi dan input/output (I/O)

Metode asinkron tidak terbatas hanya pada operasi terikat komputasi atau terikat I/O. Mereka dapat mewakili campuran keduanya. Bahkan, Anda sering menggabungkan beberapa operasi asinkron ke dalam operasi campuran yang lebih besar. Misalnya, metode RenderAsync dalam contoh sebelumnya melakukan operasi komputasi yang intensif untuk merender gambar berdasarkan masukan imageData. Ini imageData bisa berasal dari layanan web yang Anda akses secara asinkron:

public static async Task<Bitmap> DownloadDataAndRenderImageAsync(CancellationToken cancellationToken)
{
    var imageData = await DownloadImageDataAsync(cancellationToken);
    return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(cancellationToken As CancellationToken) As Task(Of Bitmap)
    Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
    Return Await RenderAsync(imageData, cancellationToken)
End Function

Note

Sampel ini menggunakan Bitmap, yang memerlukan paket System.Drawing.Common dan hanya didukung di Windows. Pola menautkan unduhan asinkron dengan operasi asinkron yang berorientasi pada komputasi berlaku pada semua platform. Gunakan pustaka pencitraan lintas platform sebagai pengganti untuk platform non-Windows.

Contoh ini juga menunjukkan bagaimana satu token pembatalan dapat dihubungkan melalui beberapa operasi asinkron. Untuk informasi selengkapnya, lihat bagian penggunaan pembatalan dalam Mengonsumsi Pola Asinkron berbasis Tugas.

Lihat juga