Mengonsumsi Pola Asinkron Berbasis Tugas

Saat Anda menggunakan Pola Asinkron berbasis tugas (TAP) untuk bekerja dengan operasi asinkron, Anda dapat menggunakan panggilan balik untuk mencapai menunggu tanpa memblokir. Untuk tugas, ini dicapai melalui metode seperti Task.ContinueWith. Dukungan asinkron berbasis bahasa menyembunyikan panggilan balik dengan memungkinkan operasi asinkron ditunggu dalam aliran kontrol normal, dan kode yang dihasilkan kompiler memberikan dukungan tingkat API yang sama ini.

Menangguhkan Eksekusi dengan Menunggu

Anda dapat menggunakan kata kunci tunggu di C# dan Operator Tunggu di Visual Basic untuk menunggu Task dan Task<TResult> objek secara asinkron. Saat Anda menunggu Task, await ekspresinya berjenis void. Saat Anda menunggu Task<TResult>, await ekspresinya berjenis TResult. Ekspresi await harus terjadi di dalam isi metode asinkron. (Fitur bahasa ini diperkenalkan pada .NET Framework 4.5.)

Di bawah penutup, fungsi tunggu menginstal panggilan balik pada tugas dengan menggunakan kelanjutan. Panggilan balik ini melanjutkan metode asinkron pada titik penangguhan. Ketika metode asinkron dilanjutkan, jika operasi yang ditunggu berhasil diselesaikan dan merupakan Task<TResult>, maka TResult dikembalikan. Jika Task atau Task<TResult> yang ditunggu berakhir di negara bagianCanceled, OperationCanceledException pengecualian akan dilepaskan. Jika Task atau Task<TResult> yang ditunggu berakhir di negara bagianFaulted, pengecualian yang menyebabkan kesalahan dilepaskan. Task Dapat salah sebagai akibat dari beberapa pengecualian, tetapi hanya salah satu pengecualian ini yang disebarluaskan. Namun, Task.Exception properti mengembalikan AggregateException pengecualian yang berisi semua kesalahan.

Jika konteks sinkronisasi (SynchronizationContext objek) dikaitkan dengan utas yang menjalankan metode asinkron pada saat penangguhan (misalnya, jika SynchronizationContext.Current properti tidak null), metode asinkron dilanjutkan pada konteks sinkronisasi yang sama dengan menggunakan metode konteks Post. Jika tidak, itu bergantung pada penjadwal tugas (TaskScheduler objek) yang saat ini pada saat penangguhan. Biasanya, ini adalah penjadwal tugas default (TaskScheduler.Default), yang menargetkan kumpulan utas. Penjadwal tugas ini menentukan apakah operasi asinkron yang ditunggu harus dilanjutkan ketika ia selesai atau apakah dimulainya kembali harus dijadwalkan. Penjadwal default biasanya memungkinkan kelanjutan berjalan pada utas yang diselesaikan oleh operasi yang ditunggu.

Ketika metode asinkron dipanggil, secara sinkron mengeksekusi tubuh fungsi sampai ekspresi tunggu pertama pada instans yang dapat ditunggu yang belum selesai, ketika titik pemanggilan kembali ke pemanggil. Jika metode asinkron tidak mengembalikan void, objek Task atau Task<TResult> dikembalikan untuk mewakili komputasi yang sedang berlangsung. Dalam metode asinkron yang tidak batal, jika pernyataan pengembalian ditemukan atau akhir isi metode tercapai, tugas selesai dalam RanToCompletion status akhir. Jika pengecualian yang tidak tertangani menyebabkan kontrol meninggalkan isi metode asinkron, tugas berakhir dalam Faulted status. Jika pengecualian tersebut adalah OperationCanceledException, tugas akan berakhir dalam status Canceled. Dengan cara ini, hasil atau pengecualian akhirnya dipublikasikan.

Ada beberapa variasi penting dari perilaku ini. Untuk alasan kinerja, jika tugas telah selesai pada saat tugas ditunggu, kontrol tidak dihasilkan, dan fungsi terus dijalankan. Selain itu, kembali ke konteks asli tidak selalu merupakan perilaku yang diinginkan dan dapat diubah; ini dijelaskan secara lebih detail di bagian berikutnya.

Mengonfigurasi Penangguhan dan Dimulainya Kembali dengan Hasil dan ConfigureAwait

Beberapa metode memberikan kontrol lebih besar atas eksekusi metode asinkron. Misalnya, Anda dapat menggunakan Task.Yield metode untuk memperkenalkan titik hasil ke dalam metode asinkron:

public class Task : …
{
    public static YieldAwaitable Yield();
    …
}

Ini setara dengan memposting atau menjadwalkan kembali ke konteks saat ini secara asinkron.

Task.Run(async delegate
{
    for(int i=0; i<1000000; i++)
    {
        await Task.Yield(); // fork the continuation into a separate work item
        ...
    }
});

Anda juga dapat menggunakan Task.ConfigureAwait metode untuk kontrol yang lebih baik atas penangguhan dan dimulainya kembali dalam metode asinkron. Seperti disebutkan sebelumnya, secara default, konteks saat ini diambil pada saat metode asinkron ditangguhkan, dan konteks yang diambil digunakan untuk memanggil kelanjutan metode asinkron setelah dimulainya kembali. Dalam banyak kasus, ini adalah perilaku yang tepat yang Anda inginkan. Dalam kasus lain, Anda mungkin tidak peduli dengan konteks kelanjutan, dan Anda dapat mencapai performa yang lebih baik dengan menghindari posting tersebut kembali ke konteks asli. Untuk mengaktifkan ini, gunakan Task.ConfigureAwait metode untuk menginformasikan operasi tunggu untuk tidak menangkap dan melanjutkan konteks, tetapi untuk melanjutkan eksekusi di mana pun operasi asinkron yang sedang ditunggu selesai:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Membatalkan Operasi Asinkron

Dimulai dengan .NET Framework 4, metode TAP yang mendukung pembatalan menyediakan sedikitnya satu kelebihan beban yang menerima token pembatalan (CancellationToken objek).

Token pembatalan dibuat melalui sumber token pembatalan (CancellationTokenSource objek). Properti sumber Token mengembalikan token pembatalan yang akan diberi sinyal ketika metode sumber Cancel dipanggil. Misalnya, jika Anda ingin mengunduh satu halaman web dan Anda ingin dapat membatalkan operasi, Anda membuat CancellationTokenSource objek, meneruskan tokennya ke metode TAP, lalu memanggil metode sumber Cancel ketika Anda siap untuk membatalkan operasi:

var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();

Untuk membatalkan beberapa pemanggilan asinkron, Anda dapat meneruskan token yang sama ke semua pemanggilan:

var cts = new CancellationTokenSource();
    IList<string> results = await Task.WhenAll(from url in urls select DownloadStringTaskAsync(url, cts.Token));
    // at some point later, potentially on another thread
    …
    cts.Cancel();

Atau, Anda dapat meneruskan token yang sama ke subset operasi selektif:

var cts = new CancellationTokenSource();
    byte [] data = await DownloadDataAsync(url, cts.Token);
    await SaveToDiskAsync(outputPath, data, CancellationToken.None);
    … // at some point later, potentially on another thread
    cts.Cancel();

Penting

Permintaan pembatalan dapat dimulai dari utas apa pun.

Anda dapat meneruskan nilai ke CancellationToken.None metode apa pun yang menerima token pembatalan untuk menunjukkan bahwa pembatalan tidak akan pernah diminta. Hal ini menyebabkan CancellationToken.CanBeCanceled properti mengembalikan false, dan metode yang disebut dapat mengoptimalkannya. Untuk tujuan pengujian, Anda juga dapat meneruskan token pembatalan yang telah dibatalkan sebelumnya yang dibuat dengan menggunakan konstruktor yang menerima nilai Boolean untuk menunjukkan apakah token harus dimulai dalam keadaan yang sudah dibatalkan atau tidak dapat dibatalkan.

Pendekatan pembatalan ini memiliki beberapa keuntungan:

  • Anda dapat meneruskan token pembatalan yang sama ke sejumlah operasi asinkron dan sinkron.

  • Permintaan pembatalan yang sama dapat diperbanyak ke sejumlah pendengar.

  • Pengembang API asinkron memegang kendali penuh apakah pembatalan dapat diminta dan kapan itu mungkin berlaku.

  • Kode yang menggunakan API dapat secara selektif menentukan pemanggilan asinkron yang akan disebarkan oleh pemanggilan pembatalan.

Kemajuan Pemantauan

Beberapa metode asinkron mengekspos kemajuan melalui antarmuka kemajuan yang diteruskan ke metode asinkron. Misalnya, pertimbangkan fungsi yang secara asinkron mengunduh serangkaian teks, dan di sepanjang jalan meningkatkan pembaruan kemajuan yang mencakup persentase unduhan yang telah selesai sejauh ini. Metode seperti itu dapat digunakan dalam aplikasi Windows Presentation Foundation (WPF) sebagai berikut:

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
            new Progress<int>(p => pbDownloadProgress.Value = p));
    }
    finally { btnDownload.IsEnabled = true; }
}

Menggunakan Penyatu Berbasis Tugas Bawaan

Namespace layanan System.Threading.Tasks mencakup beberapa metode untuk menyusun dan bekerja dengan tugas.

Task.Run

Kelas ini Task mencakup beberapa Run metode yang memungkinkan Anda dengan mudah membongkar pekerjaan sebagai Task atau Task<TResult> ke kumpulan utas, misalnya:

public async void button1_Click(object sender, EventArgs e)
{
    textBox1.Text = await Task.Run(() =>
    {
        // … do compute-bound work here
        return answer;
    });
}

Beberapa metode iniRun, seperti Task.Run(Func<Task>) kelebihan beban, ada sebagai singkatan dari TaskFactory.StartNew metode. Kelebihan beban ini memungkinkan Anda menggunakan menunggu dalam pekerjaan yang dilepas, misalnya:

public async void button1_Click(object sender, EventArgs e)
{
    pictureBox1.Image = await Task.Run(async() =>
    {
        using(Bitmap bmp1 = await DownloadFirstImageAsync())
        using(Bitmap bmp2 = await DownloadSecondImageAsync())
        return Mashup(bmp1, bmp2);
    });
}

Kelebihan beban tersebut TaskFactory.StartNew secara logis setara dengan menggunakan metode bersama dengan Unwrap metode ekstensi di Pustaka Paralel Tugas.

Task.FromResult

Gunakan FromResult metode dalam skenario ketika data mungkin sudah tersedia dan hanya perlu dikembalikan dari metode pengembalian tugas yang diangkat menjadi Task<TResult>:

public Task<int> GetValueAsync(string key)
{
    int cachedValue;
    return TryGetCachedValue(out cachedValue) ?
        Task.FromResult(cachedValue) :
        GetValueAsyncInternal();
}

private async Task<int> GetValueAsyncInternal(string key)
{
    …
}

Task.WhenAll

Gunakan WhenAll metode untuk menunggu secara asinkron pada beberapa operasi asinkron yang diwakili sebagai tugas. Metode ini memiliki beberapa kelebihan beban yang mendukung serangkaian tugas nongenerik atau sekumpulan tugas generik yang tidak seragam (misalnya, secara asinkron menunggu beberapa operasi pengembalian kekosongan, atau secara asinkron menunggu beberapa metode pengembalian nilai ketika setiap nilai mungkin memiliki jenis yang berbeda) dan untuk mendukung serangkaian tugas generik yang seragam (seperti secara asinkron menunggu beberapa TResultmetode yang dikembalikan).

Katakanlah Anda ingin mengirim pesan email ke beberapa pelanggan. Anda dapat tumpang tindih mengirim pesan sehingga Anda tidak menunggu satu pesan selesai sebelum mengirim pesan berikutnya. Anda juga dapat mengetahui kapan operasi pengiriman telah selesai dan apakah ada kesalahan yang terjadi:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);

Kode ini tidak secara eksplisit menangani pengecualian yang mungkin terjadi, tetapi memungkinkan pengecualian menyebar keluar dari await pada tugas yang dihasilkan dari WhenAll. Untuk menangani pengecualian, Anda dapat menggunakan kode seperti berikut:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    ...
}

Dalam hal ini, jika ada operasi asinkron yang gagal, semua pengecualian akan dikonsolidasikan dalam AggregateException pengecualian, yang disimpan dalam Task yang dikembalikan dari WhenAll metode. Namun, hanya salah satu pengecualian yang disebarkan oleh await kata kunci. Jika Anda ingin memeriksa semua pengecualian, Anda dapat menulis ulang kode sebelumnya sebagai berikut:

Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Mari kita pertimbangkan contoh mengunduh beberapa file dari web secara asinkron. Dalam hal ini, semua operasi asinkron memiliki jenis hasil homogen, dan mudah untuk mengakses hasilnya:

string [] pages = await Task.WhenAll(
    from url in urls select DownloadStringTaskAsync(url));

Anda dapat menggunakan teknik penanganan pengecualian yang sama yang kami bahas dalam skenario pengembalian batal sebelumnya:

Task<string> [] asyncOps =
    (from url in urls select DownloadStringTaskAsync(url)).ToArray();
try
{
    string [] pages = await Task.WhenAll(asyncOps);
    ...
}
catch(Exception exc)
{
    foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Task.WhenAny

Anda dapat menggunakan WhenAny metode untuk secara asinkron menunggu hanya salah satu dari beberapa operasi asinkron yang diwakili sebagai tugas untuk diselesaikan. Metode ini melayani empat kasus penggunaan utama:

  • Redundansi: Melakukan operasi beberapa kali dan memilih yang selesai terlebih dahulu (misalnya, menghubungi beberapa layanan web kutipan saham yang akan menghasilkan satu hasil dan memilih yang paling cepat selesai).

  • Penyelingan: Meluncurkan beberapa operasi dan menunggu semuanya selesai, tetapi memprosesnya saat selesai.

  • Pelambatan: Memungkinkan operasi tambahan dimulai saat yang lain selesai. Ini adalah perpanjangan dari skenario penyelingan.

  • Jaminan awal: Misalnya, operasi yang diwakili oleh tugas t1 dapat dikelompokkan dalam WhenAny tugas dengan tugas lain t2, dan Anda dapat menunggu WhenAny tugas. Tugas t2 dapat mewakili waktu habis, atau pembatalan, atau beberapa sinyal lain yang menyebabkan WhenAny tugas selesai sebelum t1 selesai.

Redundansi geografis

Pertimbangkan kasus ketika Anda ingin membuat keputusan tentang apakah akan membeli saham. Ada beberapa layanan web rekomendasi saham yang Anda percayai, tetapi tergantung pada beban harian, setiap layanan dapat menjadi lambat pada waktu yang berbeda. Anda dapat menggunakan WhenAny metode untuk menerima pemberitahuan ketika operasi apa pun selesai:

var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol),
    GetBuyRecommendation2Async(symbol),
    GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);

Tidak seperti WhenAll, yang mengembalikan hasil yang belum dibungkus dari semua tugas yang berhasil diselesaikan, WhenAny mengembalikan tugas yang selesai. Jika suatu tugas gagal, penting untuk mengetahui bahwa itu gagal, dan jika suatu tugas berhasil, penting untuk mengetahui tugas mana yang terkait dengan nilai pengembalian. Oleh karena itu, Anda perlu mengakses hasil tugas yang dikembalikan, atau menunggu lebih lanjut, seperti yang ditunjukkan contoh ini.

Seperti halnya WhenAll, Anda harus dapat mengakomodasi pengecualian. Karena Anda menerima kembali tugas yang telah selesai, Anda dapat menunggu tugas yang dikembalikan agar kesalahan disebarluaskan, dan try/catch tugas tersebut disebarluaskan dengan tepat; misalnya:

Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
    Task<bool> recommendation = await Task.WhenAny(recommendations);
    try
    {
        if (await recommendation) BuyStock(symbol);
        break;
    }
    catch(WebException exc)
    {
        recommendations.Remove(recommendation);
    }
}

Selain itu, bahkan jika tugas pertama berhasil diselesaikan, tugas selanjutnya mungkin gagal. Pada titik ini, Anda memiliki beberapa opsi untuk berurusan dengan pengecualian: Anda dapat menunggu sampai semua tugas yang diluncurkan selesai, dalam hal ini Anda dapat menggunakan WhenAll metode, atau Anda dapat memutuskan bahwa semua pengecualian penting dan harus dicatat. Untuk ini, Anda dapat menggunakan kelanjutan untuk menerima pemberitahuan ketika tugas telah selesai secara asinkron:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => { if (t.IsFaulted) Log(t.Exception); });
}

atau:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}

atau bahkan:

private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
    foreach(var task in tasks)
    {
        try { await task; }
        catch(Exception exc) { Log(exc); }
    }
}
…
LogCompletionIfFailed(recommendations);

Akhirnya, Anda mungkin ingin membatalkan semua operasi yang tersisa:

var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol, cts.Token),
    GetBuyRecommendation2Async(symbol, cts.Token),
    GetBuyRecommendation3Async(symbol, cts.Token)
};

Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);

Penyelingan

Pertimbangkan kasus ketika Anda mengunduh gambar dari web dan memproses setiap gambar (misalnya, menambahkan gambar ke kontrol UI). Anda memproses gambar secara berurutan pada utas UI, tetapi ingin mengunduh gambar serentak mungkin. Selain itu, Anda tidak ingin menahan menambahkan gambar ke UI sampai semuanya diunduh. Sebaliknya, Anda ingin menambahkannya saat mereka selesai.

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

Anda juga dapat menerapkan penyelingan ke skenario yang melibatkan pemrosesan intensif komputasi pada ThreadPool gambar yang diunduh; misalnya:

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)
         .ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

Pembatasan

Pertimbangkan contoh penyelingan, kecuali bahwa pengguna mengunduh begitu banyak gambar sehingga unduhan harus dibatasi; misalnya, Anda hanya ingin sejumlah unduhan terjadi secara bersamaan. Untuk mencapai hal ini, Anda dapat memulai subset dari operasi asinkron. Setelah operasi selesai, Anda dapat memulai operasi tambahan untuk menggantikannya:

const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
    imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
    nextIndex++;
}

while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch(Exception exc) { Log(exc); }

    if (nextIndex < urls.Length)
    {
        imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
        nextIndex++;
    }
}

Jaminan Awal

Pertimbangkan bahwa Anda sedang menunggu secara asinkron agar operasi selesai sekaligus menanggapi permintaan pembatalan pengguna (misalnya, pengguna mengeklik tombol batalkan). Kode berikut menggambarkan perilaku ini:

private CancellationTokenSource m_cts;

public void btnCancel_Click(object sender, EventArgs e)
{
    if (m_cts != null) m_cts.Cancel();
}

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();
    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        if (imageDownload.IsCompleted)
        {
            Bitmap image = await imageDownload;
            panel.AddImage(image);
        }
        else imageDownload.ContinueWith(t => Log(t));
    }
    finally { btnRun.Enabled = true; }
}

private static async Task UntilCompletionOrCancellation(
    Task asyncOp, CancellationToken ct)
{
    var tcs = new TaskCompletionSource<bool>();
    using(ct.Register(() => tcs.TrySetResult(true)))
        await Task.WhenAny(asyncOp, tcs.Task);
    return asyncOp;
}

Implementasi ini memungkinkan kembali antarmuka pengguna segera setelah Anda memutuskan untuk keluar, tetapi tidak membatalkan operasi asinkron yang mendasarinya. Alternatif lain adalah membatalkan operasi yang tertunda ketika Anda memutuskan untuk keluar, tetapi tidak membangun kembali antarmuka pengguna sampai operasi selesai, berpotensi berakhir lebih awal karena permintaan pembatalan:

private CancellationTokenSource m_cts;

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();

    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        Bitmap image = await imageDownload;
        panel.AddImage(image);
    }
    catch(OperationCanceledException) {}
    finally { btnRun.Enabled = true; }
}

Contoh lain dari jaminan awal melibatkan penggunaan WhenAny metode bersama dengan Delay metode, seperti yang dibahas di bagian berikutnya.

Task.Delay

Anda dapat menggunakan Task.Delay metode untuk memperkenalkan jeda ke dalam eksekusi metode asinkron. Ini berguna untuk berbagai jenis fungsi, termasuk membangun perulangan polling dan menunda penanganan input pengguna untuk jangka waktu yang telah ditentukan. Metode ini Task.Delay juga dapat berguna dalam kombinasi dengan Task.WhenAny untuk menerapkan waktu habis saat menunggu.

Jika tugas yang merupakan bagian dari operasi asinkron yang lebih besar (misalnya, layanan web ASP.NET) membutuhkan waktu terlalu lama untuk diselesaikan, operasi keseluruhan bisa menderita, terutama jika gagal menyelesaikannya. Untuk alasan ini, penting untuk dapat mengatur waktu ketika menunggu operasi asinkron. Metode Task.Wait, Task.WaitAll, dan Task.WaitAny sinkron menerima nilai waktu habis, tetapi metode yang sesuai TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny dan yang disebutkanTask.WhenAll/Task.WhenAnysebelumnya tidak. Sebagai gantinya, Anda dapat menggunakan Task.Delay dan Task.WhenAny dalam kombinasi untuk mengimplementasikan waktu habis.

Misalnya, di aplikasi UI Anda, katakanlah Anda ingin mengunduh gambar dan menonaktifkan UI saat gambar diunduh. Namun, jika unduhan memakan waktu terlalu lama, Anda ingin mengaktifkan kembali UI dan membuang unduhan:

public async void btnDownload_Click(object sender, EventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap> download = GetBitmapAsync(url);
        if (download == await Task.WhenAny(download, Task.Delay(3000)))
        {
            Bitmap bmp = await download;
            pictureBox.Image = bmp;
            status.Text = "Downloaded";
        }
        else
        {
            pictureBox.Image = null;
            status.Text = "Timed out";
            var ignored = download.ContinueWith(
                t => Trace("Task finally completed"));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Hal yang sama berlaku untuk beberapa unduhan, karena WhenAll mengembalikan tugas:

public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap[]> downloads =
            Task.WhenAll(from url in urls select GetBitmapAsync(url));
        if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
        {
            foreach(var bmp in downloads.Result) panel.AddImage(bmp);
            status.Text = "Downloaded";
        }
        else
        {
            status.Text = "Timed out";
            downloads.ContinueWith(t => Log(t));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Membangun Penyatu Berbasis Tugas

Karena tugas dapat sepenuhnya mewakili operasi asinkron dan memberikan kemampuan sinkron dan asinkron untuk bergabung dengan operasi, mengambil hasilnya, dan sebagainya, Anda dapat membangun pustaka penyatu yang berguna yang menyusun tugas untuk membangun pola yang lebih besar. Seperti yang dibahas di bagian sebelumnya, .NET menyertakan beberapa penyatu bawaan, tetapi Anda juga dapat membangun sendiri. Bagian berikut memberikan beberapa contoh metode dan jenis penyatu potensial.

RetryOnFault

Dalam banyak situasi, Anda mungkin ingin mencoba kembali operasi jika upaya sebelumnya gagal. Untuk kode sinkron, Anda mungkin membuat metode pembantu seperti RetryOnFault dalam contoh berikut untuk mencapai hal ini:

public static T RetryOnFault<T>(
    Func<T> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return function(); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

Anda dapat membangun metode pembantu yang hampir identik untuk operasi asinkron yang diimplementasikan dengan TAP dan dengan demikian mengembalikan tugas:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

Anda kemudian dapat menggunakan penyatu ini untuk menyandikan upaya ulang ke dalam logika aplikasi; misalnya:

// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3);

Anda dapat memperluas RetryOnFault fungsi lebih lanjut. Misalnya, fungsi dapat menerima fungsi lain Func<Task> yang akan dipanggil di antara percobaan ulang untuk menentukan kapan harus mencoba operasi lagi; misalnya:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
        await retryWhen().ConfigureAwait(false);
    }
    return default(T);
}

Anda kemudian dapat menggunakan fungsi sebagai berikut untuk menunggu sedetik sebelum mencoba kembali operasi:

// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3, () => Task.Delay(1000));

NeedOnlyOne

Terkadang, Anda dapat memanfaatkan redundansi untuk meningkatkan latensi operasi dan peluang untuk sukses. Pertimbangkan beberapa layanan web yang menyediakan penawaran saham, tetapi pada berbagai waktu dalam sehari, setiap layanan dapat memberikan tingkat kualitas dan waktu respons yang berbeda. Untuk mengatasi fluktuasi ini, Anda dapat mengeluarkan permintaan ke semua layanan web, dan segera setelah Anda mendapatkan tanggapan dari satu, batalkan permintaan yang tersisa. Anda dapat menerapkan fungsi pembantu untuk membuatnya lebih mudah untuk menerapkan pola umum meluncurkan beberapa operasi ini, menunggu apa pun, dan kemudian membatalkan sisanya. NeedOnlyOne Contoh berikut mengilustrasikan skenario ini:

public static async Task<T> NeedOnlyOne(
    params Func<CancellationToken,Task<T>> [] functions)
{
    var cts = new CancellationTokenSource();
    var tasks = (from function in functions
                 select function(cts.Token)).ToArray();
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach(var task in tasks)
    {
        var ignored = task.ContinueWith(
            t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return completed;
}

Anda kemudian dapat menggunakan fungsi ini sebagai berikut:

double currentPrice = await NeedOnlyOne(
    ct => GetCurrentPriceFromServer1Async("msft", ct),
    ct => GetCurrentPriceFromServer2Async("msft", ct),
    ct => GetCurrentPriceFromServer3Async("msft", ct));

Operasi yang Diselingi

Ada potensi masalah performa dengan menggunakan WhenAny metode untuk mendukung skenario penyelingan saat Anda bekerja dengan serangkaian tugas besar. Setiap panggilan untuk menghasilkan kelanjutan WhenAny yang didaftarkan dengan setiap tugas. Untuk N jumlah tugas, ini menghasilkan kelanjutan O(N2) yang dibuat selama masa pakai operasi penyelingan. Jika Anda bekerja dengan sekumpulan tugas besar, Anda bisa menggunakan penyatu (Interleaved dalam contoh berikut) untuk mengatasi masalah performa:

static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
    var inputTasks = tasks.ToList();
    var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
                   select new TaskCompletionSource<T>()).ToList();
    int nextTaskIndex = -1;
    foreach (var inputTask in inputTasks)
    {
        inputTask.ContinueWith(completed =>
        {
            var source = sources[Interlocked.Increment(ref nextTaskIndex)];
            if (completed.IsFaulted)
                source.TrySetException(completed.Exception.InnerExceptions);
            else if (completed.IsCanceled)
                source.TrySetCanceled();
            else
                source.TrySetResult(completed.Result);
        }, CancellationToken.None,
           TaskContinuationOptions.ExecuteSynchronously,
           TaskScheduler.Default);
    }
    return from source in sources
           select source.Task;
}

Anda kemudian dapat menggunakan penyatu untuk memproses hasil tugas saat selesai; misalnya:

IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
    int result = await task;
    …
}

WhenAllOrFirstException

Dalam skenario pencar/kumpulkan tertentu, Anda mungkin ingin menunggu semua tugas dalam satu set, kecuali salah satunya kesalahan, dalam hal ini Anda ingin berhenti menunggu segera setelah pengecualian terjadi. Anda dapat mencapainya dengan metode penyatu seperti WhenAllOrFirstException dalam contoh berikut:

public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
    var inputs = tasks.ToList();
    var ce = new CountdownEvent(inputs.Count);
    var tcs = new TaskCompletionSource<T[]>();

    Action<Task> onCompleted = (Task completed) =>
    {
        if (completed.IsFaulted)
            tcs.TrySetException(completed.Exception.InnerExceptions);
        if (ce.Signal() && !tcs.Task.IsCompleted)
            tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
    };

    foreach (var t in inputs) t.ContinueWith(onCompleted);
    return tcs.Task;
}

Membangun Struktur Data Berbasis Tugas

Selain kemampuan untuk membangun penyatu berbasis tugas kustom, memiliki struktur data di Task dan Task<TResult> yang mewakili hasil operasi asinkron dan sinkronisasi yang diperlukan untuk bergabung dengannya menjadikannya jenis yang kuat untuk membangun struktur data kustom yang akan digunakan dalam skenario asinkron.

AsyncCache

Salah satu aspek penting dari tugas adalah bahwa tugas tersebut dapat diserahkan kepada beberapa konsumen, yang semuanya dapat menunggunya, mendaftarkan kelanjutan dengannya, mendapatkan hasil atau pengecualiannya (dalam kasus Task<TResult>), dan sebagainya. Ini membuat Task dan Task<TResult> sangat cocok untuk digunakan dalam infrastruktur penembolokan asinkron. Berikut adalah contoh cache asinkron kecil tetapi kuat yang dibangun di atas Task<TResult>:

public class AsyncCache<TKey, TValue>
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

    public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
    {
        if (valueFactory == null) throw new ArgumentNullException("valueFactory");
        _valueFactory = valueFactory;
        _map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
    }

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException("key");
            return _map.GetOrAdd(key, toAdd =>
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}

Kelas AsyncCache<TKey,TValue> menerima sebagai delegasi ke konstruktornya fungsi yang mengambil TKey dan mengembalikan Task<TResult>. Setiap nilai yang diakses sebelumnya dari cache disimpan dalam kamus internal, dan AsyncCache memastikan bahwa hanya satu tugas yang dihasilkan per kunci, bahkan jika cache diakses secara bersamaan.

Misalnya, Anda dapat membuat cache untuk halaman web yang diunduh:

private AsyncCache<string,string> m_webPages =
    new AsyncCache<string,string>(DownloadStringTaskAsync);

Anda kemudian dapat menggunakan cache ini dalam metode asinkron kapan pun Anda membutuhkan konten halaman web. Kelas AsyncCache memastikan bahwa Anda mengunduh halaman sesegera mungkin, dan menyimpan hasilnya dalam cache.

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtContents.Text = await m_webPages["https://www.microsoft.com"];
    }
    finally { btnDownload.IsEnabled = true; }
}

AsyncProducerConsumerCollection

Anda juga dapat menggunakan tugas untuk membangun struktur data untuk mengoordinasikan aktivitas asinkron. Pertimbangkan salah satu pola desain paralel klasik: produsen/konsumen. Dalam pola ini, produsen menghasilkan data yang digunakan oleh konsumen, dan produsen dan konsumen dapat berjalan secara paralel. Misalnya, konsumen memproses item 1, yang sebelumnya dihasilkan oleh produsen yang sekarang memproduksi barang 2. Untuk pola produsen/konsumen, Anda selalu memerlukan beberapa struktur data untuk menyimpan karya yang dibuat oleh produsen sehingga konsumen dapat diberitahu tentang data baru dan menemukannya bila tersedia.

Berikut adalah struktur data sederhana, dibangun di atas tugas, yang memungkinkan metode asinkron untuk digunakan sebagai produsen dan konsumen:

public class AsyncProducerConsumerCollection<T>
{
    private readonly Queue<T> m_collection = new Queue<T>();
    private readonly Queue<TaskCompletionSource<T>> m_waiting =
        new Queue<TaskCompletionSource<T>>();

    public void Add(T item)
    {
        TaskCompletionSource<T> tcs = null;
        lock (m_collection)
        {
            if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
            else m_collection.Enqueue(item);
        }
        if (tcs != null) tcs.TrySetResult(item);
    }

    public Task<T> Take()
    {
        lock (m_collection)
        {
            if (m_collection.Count > 0)
            {
                return Task.FromResult(m_collection.Dequeue());
            }
            else
            {
                var tcs = new TaskCompletionSource<T>();
                m_waiting.Enqueue(tcs);
                return tcs.Task;
            }
        }
    }
}

Dengan struktur data yang ada, Anda dapat menulis kode seperti berikut:

private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.Take();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Add(data);
}

Namespace layanan System.Threading.Tasks.Dataflow mencakup BufferBlock<T> jenis, yang dapat Anda gunakan dengan cara yang sama, tetapi tanpa harus membangun jenis koleksi kustom:

private static BufferBlock<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.ReceiveAsync();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Post(data);
}

Catatan

Namespace layanan System.Threading.Tasks.Dataflow tersedia sebagai paket NuGet. Untuk menginstal assembly yang berisi System.Threading.Tasks.Dataflow namespace layanan, buka proyek Anda di Visual Studio, pilih Kelola Paket NuGet dari menu Proyek, dan cari System.Threading.Tasks.Dataflow paket secara online.

Lihat juga