Share via


Menerapkan Pola Asinkron Berbasis Tugas

Menjelaskan cara mengimplementasikan Pola Asinkron Berbasis Tugas (TAP) dalam tiga cara: dengan menggunakan pengkompilasi C# dan Visual Basic di Visual Studio, secara manual, atau melalui kombinasi metode pengkompilasi dan manual. Bagian berikut membahas metode dan peristiwa ini secara lebih rinci. Anda dapat menggunakan pola TAP untuk mengimplementasikan operasi asinkron terikat komputasi dan terikat 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 async kata kunci (Async dalam Visual Basic) dianggap sebagai metode asinkron, dan pengompilasi C# dan Visual Basic melakukan transformasi yang diperlukan untuk mengimplementasikan metode secara asinkron dengan menggunakan TAP. Metode asinkron harus mengembalikan objek System.Threading.Tasks.Task 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 dirangkai ke tugas output dan menyebabkan tugas yang dihasilkan berakhir dalam TaskStatus.Faulted status. Pengecualian untuk aturan ini adalah ketika OperationCanceledException (atau jenis turunan) tidak tertangani, dalam hal ini tugas yang dihasilkan berakhir dalam TaskStatus.Canceled status.

Membuat metode TAP secara manual

Anda dapat menerapkan pola TAP secara manual untuk kontrol yang lebih baik atas implementasi. Pengkompilasi mengandalkan area permukaan publik yang diekspos dari System.Threading.Tasks namespace layanan dan jenis pendukung di System.Runtime.CompilerServices namespace layanan. Untuk mengimplementasikan TAP sendiri, Anda membuat TaskCompletionSource<TResult> objek, melakukan operasi asinkron, dan ketika selesai, panggil SetResult, SetException, atau SetCanceled metode, 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:

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

Pendekatan hibrid

Anda mungkin merasa berguna untuk menerapkan pola TAP secara manual tetapi untuk 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 pengkompilasi sehingga pengecualian bisa lolos ke pemanggil langsung metode daripada diekspos melalui System.Threading.Tasks.Task objek:

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

private async Task<int> MethodAsyncInternal(string input)
{

   // code that uses await goes here

   return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
    If input Is Nothing Then Throw New ArgumentNullException("input")

    Return MethodAsyncInternal(input)
End Function

Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)

    ' code that uses await goes here

    return value
End Function

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

Beban kerja

Anda dapat menerapkan operasi asinkron terikat komputasi dan terikat I/O sebagai metode TAP. Namun, ketika metode TAP diekspos secara publik dari pustaka, metode tersebut harus disediakan hanya untuk beban kerja yang melibatkan operasi terikat I/O (mereka mungkin juga melibatkan komputasi, tetapi tidak boleh murni komputasi). Jika metode murni terikat komputasi, metode harus diekspos hanya sebagai implementasi sinkron. Kode yang mengonsumsinya kemudian dapat memilih apakah akan membungkus pemanggilan dari metode sinkron tersebut ke dalam tugas untuk membongkar pekerjaan ke alur lain atau untuk mencapai paralelisme. Dan jika metode terikat I/O, metode harus diekspos hanya sebagai implementasi asinkron.

Tugas terikat komputasi

Kelas System.Threading.Tasks.Task ini sangat cocok untuk mewakili operasi intensif komputasi. Secara default, ia memanfaatkan dukungan khusus dalam ThreadPool kelas untuk memberikan eksekusi yang efisien, dan juga memberikan kontrol yang signifikan atas kapan, di mana, dan bagaimana komputasi asinkron dijalankan.

Anda dapat membuat tugas terikat komputasi dengan cara berikut:

  • Dalam .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. Anda dapat menggunakan Run untuk meluncurkan tugas terikat komputasi dengan mudah yang menargetkan kumpulan alur. Ini adalah mekanisme yang disukai untuk meluncurkan tugas terikat komputasi. Gunakan StartNew secara langsung hanya ketika Anda menginginkan kontrol yang lebih halus atas tugas.

  • Dalam .NET Framework 4, gunakan TaskFactory.StartNew metode, yang menerima delegasi (biasanya Action<T> atau Func<TResult>) untuk dieksekusi secara asinkron. Jika Anda memberikan Action<T> delegasi, metode mengembalikan System.Threading.Tasks.Task objek yang mewakili eksekusi asinkron 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), yang semuanya memberikan kontrol mendetail atas penjadwalan dan eksekusi tugas. Instans pabrik yang menargetkan penjadwal tugas saat ini tersedia sebagai properti statis (Factory) Task kelas; 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 telah dimulai.

  • Gunakan kelebihan beban Task.ContinueWith metode. Metode ini membuat tugas baru yang dijadwalkan ketika tugas lain selesai. Beberapa ContinueWith 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 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 bahasa umum juga dapat memantau token.

Misalnya, pertimbangkan metode asinkron yang merender gambar. Isi tugas dapat melakukan polling token pembatalan sehingga kode dapat keluar lebih awal jika permintaan pembatalan tiba selama penyajian. Selain itu, jika permintaan pembatalan tiba sebelum penyajian dimulai, Anda mungkin ingin mencegah operasi penyajian:

internal 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

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 hal 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 Faulted status, dan setiap upaya untuk menunggu tugas atau mengakses hasilnya menyebabkan pengecualian dilemparkan.

Tugas terikat I/O

Untuk membuat tugas yang tidak boleh langsung didukung oleh alur untuk keseluruhan eksekusinya, gunakan jenis TaskCompletionSource<TResult>. Jenis ini mengekspos Task properti yang mengembalikan instans terkait Task<TResult>. Siklus hidup tugas ini dikendalikan oleh TaskCompletionSource<TResult> metode seperti SetResult, SetException, SetCanceled dan TrySet variannya.

Katakanlah Anda ingin membuat tugas yang akan selesai setelah jangka 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, dan dengan menggunakan TaskCompletionSource<TResult> Anda dapat meletakkan Task<TResult> depan pada pewaktu, misalnya:

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, dan 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, Anda dapat menggunakan sumber dengan dummy TResult (Boolean adalah pilihan default yang baik, tetapi jika Anda khawatir tentang pengguna dari Task downcasting ke Task<TResult>, Anda dapat menggunakan jenis 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 dapat dikodekan sebagai berikut (perhatikan perubahan jenis pengembalian dan perubahan argumen menjadi TrySetResult):

public static Task<bool> Delay(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 Delay(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 terikat komputasi campuran dan terikat I/O

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

public 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

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

Lihat juga