Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Saat Anda menggunakan Pola Asinkron Berbasis Tugas (TAP) untuk bekerja dengan operasi asinkron, Anda dapat menggunakan panggilan balik untuk mencapai penantian tanpa memblokir. Untuk tugas, ini dicapai melalui metode seperti Task.ContinueWith. Dukungan asinkron berbasis bahasa menyembunyikan panggilan balik dengan memungkinkan operasi asinkron ditunggu dalam alur kontrol normal, dan kode yang dihasilkan kompilator menyediakan 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, ekspresi await berjenis void. Saat Anda menunggu Task<TResult>, ekspresi await berjenis TResult. Ekspresi await harus terjadi di dalam isi metode asinkron. (Fitur bahasa ini diperkenalkan dalam .NET Framework 4.5.)
Di bawah sampul, fungsionalitas 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>, TResult tersebut dikembalikan. Jika Task atau Task<TResult> yang ditunggu berakhir dalam status Canceled, pengecualian OperationCanceledException akan dihasilkan. Jika Task atau Task<TResult> yang ditunggu berakhir dalam keadaan Faulted, pengecualian yang menyebabkan kondisi kesalahan tersebut akan dilemparkan. A Task dapat mengalami kesalahan akibat beberapa pengecualian, tetapi hanya salah satu pengecualian ini yang dipropagasi. 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 properti SynchronizationContext.Current tidak null), metode asinkron dilanjutkan pada konteks sinkronisasi yang sama dengan menggunakan metode Post dari konteks tersebut. Jika tidak, itu bergantung pada penjadwal tugas (TaskScheduler objek) yang saat ini pada saat penangguhan. Biasanya, ini adalah penjadwal tugas default (TaskScheduler.Default) yang mengarah ke pool thread. Penjadwal tugas ini menentukan apakah operasi asinkron yang ditunggu-tunggu harus dilanjutkan di tempat operasi selesai atau apakah kelanjutannya harus dijadwalkan. Penjadwal default biasanya memungkinkan kelanjutan dapat berjalan pada utas di mana operasi yang diantisipasi selesai.
Ketika metode asinkron dipanggil, metode tersebut secara sinkron menjalankan isi fungsi hingga ekspresi tunggu pertama pada instans yang dapat ditunggu yang belum selesai, di mana 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 non-void, jika pernyataan pengembalian ditemui atau akhir dari tubuh metode tercapai, tugas selesai dalam status RanToCompletion akhir. Jika pengecualian yang tidak tertangani menyebabkan kendali keluar dari tubuh metode asinkron, tugas berakhir dalam keadaan Faulted. Jika pengecualian tersebut adalah OperationCanceledException, tugas akan berakhir dalam status Canceled . Dengan cara ini, hasil atau pengecualian akhirnya diterbitkan.
Ada beberapa variasi penting dari perilaku ini. Untuk alasan performa, 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 rinci di bagian berikutnya.
Mengonfigurasi Penangguhan dan Melanjutkan dengan Yield 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 secara asinkron ke konteks saat ini.
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 metode Task.ConfigureAwait untuk kontrol yang lebih baik atas penangguhan dan dilanjutkannya kembali dalam metode asinkron. Seperti disebutkan sebelumnya, secara default, konteks saat ini diambil pada saat metode asinkron ditangguhkan, dan konteks yang ditangkap digunakan untuk memanggil kelanjutan metode asinkron setelah dimulai 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 metode Task.ConfigureAwait untuk memberitahu operasi 'await' agar tidak menangkap dan melanjutkan pada konteks, tetapi untuk melanjutkan eksekusi di mana pun operasi asinkron yang sedang ditunggu telah selesai.
await someTask.ConfigureAwait(continueOnCapturedContext:false);
Membatalkan Operasi Asinkron
Dimulai dengan .NET Framework 4, metode TAP yang mendukung pembatalan menyediakan setidaknya 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 diajukan dari utas apa pun.
Anda dapat mengirimkan nilai CancellationToken.None ke metode apa pun yang menerima token pembatalan untuk menunjukkan bahwa pembatalan tidak akan pernah diminta. Ini menyebabkan CancellationToken.CanBeCanceled properti mengembalikan false, dan metode yang dipanggil dapat dioptimalkan sesuai. 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 status yang sudah dibatalkan atau tidak dapat dibatalkan.
Pendekatan pembatalan ini memiliki beberapa keuntungan:
Anda dapat meneruskan token pembatalan yang sama ke berbagai operasi asinkron dan sinkron.
Permintaan pembatalan yang sama dapat diproliferasi ke sejumlah pendengar.
Pengembang API asinkron memegang kendali penuh atas apakah pembatalan dapat diminta dan kapan mungkin berlaku.
Kode yang memanfaatkan API dapat secara selektif menentukan pemanggilan asinkron yang akan menerima penyebarluasan permintaan pembatalan.
Progres Pemantauan
Beberapa metode asinkron mengekspos kemajuan melalui antarmuka yang menyediakan informasi kemajuan dan diteruskan ke metode asinkron. Misalnya, pertimbangkan fungsi yang secara asinkron mengunduh sebuah string teks, dan selama proses menyediakan pembaruan kemajuan yang mencakup persentase unduhan yang telah selesai sejauh ini. Metode seperti itu dapat dikonsumsi 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 Combinator Berbasis Tugas Bawaan
Namespace System.Threading.Tasks mencakup beberapa metode untuk menyusun dan bekerja dengan tugas.
Task.Run
Kelas Task ini mencakup beberapa Run fungsi yang memungkinkan Anda dengan mudah mengalihkan 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 metode .TaskFactory.StartNew Kelebihan ini memungkinkan Anda menggunakan fungsi await di dalam tugas yang dialihkan, 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
FromResult Gunakan metode dalam skenario di mana 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 metode WhenAll untuk secara asinkron menunggu beberapa operasi asinkron yang diwakili sebagai tugas. Metode ini memiliki beberapa overload yang mendukung serangkaian tugas non-generik atau sekumpulan tugas generik yang tidak seragam (misalnya, secara asinkron menunggu beberapa operasi yang mengembalikan void, atau secara asinkron menunggu beberapa metode yang mengembalikan nilai di mana setiap nilai mungkin memiliki jenis yang berbeda) dan untuk mendukung serangkaian tugas generik yang seragam (seperti secara asinkron menunggu beberapa metode yang mengembalikan TResult).
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 bisa menggunakan kode seperti berikut:
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
...
}
Dalam kasus ini, jika ada operasi asinkron yang gagal, semua exception akan dikonsolidasikan dalam pengecualian AggregateException, yang disimpan dalam Task yang dikembalikan dari metode WhenAll. Namun, hanya salah satu pengecualian yang disebarluaskan 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 yang 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 kekosongan 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 metode WhenAny untuk menunggu secara asinkron hingga salah satu dari beberapa operasi asinkron yang diwakili sebagai tugas selesai. 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 menyelesaikan yang tercepat).
Interleaving: Melakukan banyak operasi dan menunggu semuanya selesai, sembari memprosesnya saat masing-masing selesai.
Pengaturan: Memungkinkan operasi tambahan dimulai seiring penyelesaian operasi lainnya. Ini adalah pengembangan dari skenario interleaving.
Bailout awal: Misalnya, operasi yang diwakili oleh tugas t1 dapat dikelompokkan dalam tugas WhenAny bersama tugas lain t2, dan Anda bisa menunggu pada tugas WhenAny. Tugas t2 dapat mewakili waktu habis, atau pembatalan, atau beberapa sinyal lain yang menyebabkan WhenAny tugas selesai sebelum t1 selesai.
Pemborosan
Pertimbangkan kasus di mana Anda ingin membuat keputusan tentang apakah akan membeli saham. Ada beberapa layanan web rekomendasi stok yang Anda percayai, tetapi tergantung pada beban harian, setiap layanan akhirnya bisa 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 tugas gagal, penting untuk mengetahui bahwa tugas gagal, dan jika 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 tugas yang selesai kembali, 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 berikutnya 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);
Terakhir, 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);
Penggantian berselang
Pertimbangkan kasus di mana Anda mengunduh gambar dari web dan memproses setiap gambar (misalnya, menambahkan gambar ke kontrol UI). Anda memproses gambar secara berurutan di utas UI, tetapi ingin mengunduh gambar secara bersamaan semaksimal mungkin. Selain itu, Anda tidak ingin menahan penambahan gambar ke UI hingga semuanya diunduh. Sebagai gantinya, Anda ingin menambahkannya secara bertahap 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 interleaving 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{}
}
Pengendalian kecepatan
Pertimbangkan contoh interleaving, kecuali bahwa pengguna mengunduh begitu banyak gambar sehingga unduhan harus dibatasi; misalnya, Anda hanya ingin sejumlah unduhan tertentu terjadi secara bersamaan. Untuk mencapai hal ini, Anda dapat memulai subset operasi asinkron. Setelah operasi selesai, Anda dapat memulai operasi tambahan untuk menggantikan operasi tersebut:
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++;
}
}
Bailout Awal
Pertimbangkan bahwa Anda menunggu secara asinkron agar operasi selesai saat secara bersamaan menanggapi permintaan pembatalan pengguna (misalnya, pengguna mengklik tombol batal). Kode berikut mengilustrasikan skenario 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 mengaktifkan kembali antarmuka pengguna segera setelah Anda memutuskan untuk menghentikan proses, tetapi tidak membatalkan operasi asinkron yang mendasarinya. Alternatif lain adalah membatalkan operasi yang tertunda ketika Anda memutuskan untuk menghentikan, tetapi tidak membangun kembali antarmuka pengguna sampai operasi selesai, yang mungkin karena berakhir lebih awal akibat 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 bailout awal melibatkan penggunaan metode WhenAny bersama dengan metode Delay, seperti yang dibahas di bagian berikutnya.
Metode Task.Delay
Anda dapat menggunakan Task.Delay metode untuk memperkenalkan jeda ke dalam eksekusi metode asinkron. Ini berguna untuk berbagai jenis fungsionalitas, termasuk membangun perulangan polling dan menunda penanganan input pengguna untuk jangka waktu yang telah ditentukan. Metode Task.Delay juga dapat berguna dalam kombinasi dengan Task.WhenAny untuk mengimplementasikan batas waktu pada proses 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 dapat menderita, terutama jika gagal selesai. Untuk alasan ini, penting untuk dapat mengatur batas waktu ketika menunggu operasi asinkron. Metode sinkron Task.Wait, Task.WaitAll, dan Task.WaitAny menerima nilai waktu habis, tetapi TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny yang sesuai dan metode Task.WhenAll/Task.WhenAny yang disebutkan sebelumnya 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 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 Combinator 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 kombinator yang berguna yang menyusun tugas untuk membangun pola yang lebih besar. Seperti yang dibahas di bagian sebelumnya, .NET menyertakan beberapa combinator bawaan, tetapi Anda juga dapat membangun sendiri. Bagian berikut ini menyediakan beberapa contoh metode dan jenis combinator potensial.
Coba lagiOnFault
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 combinator ini untuk mengodekan coba lagi 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 dan peluang operasi untuk keberhasilan. Pertimbangkan beberapa layanan web yang memberikan kuotasi stok, tetapi pada berbagai waktu dalam sehari, setiap layanan dapat memberikan tingkat kualitas dan waktu respons yang berbeda. Untuk menangani fluktuasi ini, Anda dapat mengeluarkan permintaan ke semua layanan web, dan segera setelah Anda mendapatkan respons dari satu, batalkan permintaan yang tersisa. Anda dapat menerapkan fungsi pembantu untuk mempermudah penerapan pola umum peluncuran beberapa operasi ini, menunggu operasi apa pun, lalu membatalkan sisanya. Fungsi NeedOnlyOne dalam 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 metode WhenAny untuk mendukung skenario interleaving ketika Anda menangani kumpulan tugas yang besar. Setiap panggilan ke WhenAny menghasilkan pendaftaran kelanjutan dengan setiap tugas. Untuk N jumlah tugas, ini menghasilkan kelanjutan O(N2) yang dibuat selama masa pakai operasi interleaving. Jika Anda bekerja dengan sekumpulan tugas besar, Anda bisa menggunakan combinator (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 combinator untuk memproses hasil tugas saat selesai; misalnya:
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;
…
}
WhenAllOrFirstException
Dalam skenario sebar/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 combinator 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 combinator 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 sebuah tugas adalah bahwa tugas tersebut dapat diberikan kepada beberapa konsumen, yang semuanya dapat menunggu hasilnya, mendaftarkan kelanjutannya, mendapatkan hasil atau pengecualiannya (dalam kasus Task<TResult>), dan seterusnya. Ini membuat Task dan Task<TResult> sangat cocok untuk digunakan dalam infrastruktur peng-cache-an 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 fungsi sebagai delegasi untuk konstruktornya 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 setiap kali Anda memerlukan konten halaman web. Kelas AsyncCache memastikan bahwa Anda mengunduh sesedikit mungkin halaman 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 dikonsumsi oleh konsumen, dan produsen dan konsumen dapat berjalan secara paralel. Misalnya, konsumen memproses item 1, yang sebelumnya dihasilkan oleh produsen yang sekarang memproduksi item 2. Untuk pola produsen/konsumen, Anda sangat memerlukan beberapa struktur data untuk menyimpan pekerjaan yang dibuat oleh produsen sehingga konsumen dapat diberi tahu tentang data baru dan menemukannya saat tersedia.
Berikut adalah struktur data sederhana, dibangun di atas tugas, yang memungkinkan metode asinkron 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 tersebut, 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 System.Threading.Tasks.Dataflow menyertakan 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);
}
Nota
Namespace System.Threading.Tasks.Dataflow tersedia sebagai paket NuGet. Untuk menginstal rakitan yang berisi System.Threading.Tasks.Dataflow namespace, buka proyek Anda di Visual Studio, pilih Kelola Paket NuGet dari menu Proyek, dan cari System.Threading.Tasks.Dataflow paket secara online.