Task dan Task<T>

Komposisi Task dengan WhenAll dan WhenAny.

Daftar isi

Task mewakili satu pekerjaan asinkron yang sedang berjalan atau belum selesai, sedangkan Task<T> menyimpan hasil pekerjaan itu setelah tuntas. Topik ini membahas cara membaca keadaan sebuah Task serta cara mengomposisikan banyak tugas dengan Task.WhenAll dan Task.WhenAny. Setelah membacanya, sekelompok pekerjaan asinkron dapat dijalankan bersamaan, hasilnya dapat digabungkan, dan tugas yang selesai paling dulu dapat dipilih secara sengaja.

Task sebagai wakil pekerjaan#

Satu objek Task mewakili pekerjaan asinkron yang sedang berlangsung, bukan pekerjaannya itu sendiri. Objek ini membawa keadaan pekerjaan, yaitu belum selesai saat dibuat dan selesai beserta hasilnya saat pekerjaannya tuntas. Tipe Task dipakai untuk pekerjaan tanpa nilai kembali, sedangkan Task<T> untuk pekerjaan yang menghasilkan satu nilai bertipe T.

Metode async adalah sumber Task yang paling umum. Setiap pemanggilan metode async mengembalikan satu Task yang berdiri sendiri, sehingga beberapa pemanggilan dapat dijalankan lebih dahulu lalu dikumpulkan ke dalam array sebelum ditunggu bersama. Pemanggil dapat menunggu Task dengan await, memeriksa keadaannya tanpa menunggu melalui properti IsCompleted, atau melampirkan kelanjutan padanya. Await adalah bentuk yang paling sering dipakai karena kodenya terbaca seperti alur biasa.

Kebalikan dari menunggu adalah membaca keadaan Task secara langsung. Properti IsFaulted menyatakan pekerjaan berakhir dengan exception, properti IsCanceled menyatakan pekerjaan dihentikan lewat pembatalan, dan properti Status merinci seluruh keadaannya. Await atas tugas yang gagal melempar exceptionnya, sedangkan await atas tugas yang dibatalkan melempar OperationCanceledException, sehingga keadaan tersebut tidak perlu diperiksa manual apabila hasilnya langsung di-await.

Task juga dapat dibuat tanpa menjalankan pekerjaan sama sekali. Task.FromResult membungkus satu nilai menjadi tugas yang sudah selesai, dan cara ini dipakai apabila hasilnya sudah tersedia, misalnya dari cache. Pemanggil yang hanya mengenal tanda tangan Task tetap dapat menunggunya seperti biasa, sedangkan implementasi bebas memilih jalur serempak atau asinkron di baliknya.

Menggabungkan tugas dengan WhenAll#

Task.WhenAll menerima kumpulan tugas lalu mengembalikan satu tugas gabungan yang selesai setelah semua anggotanya selesai. Program berikut menjalankan tiga penghitungan huruf bersamaan, kemudian menggabungkan hasilnya. Salin programnya ke berkas Program.cs, lalu jalankan dengan perintah dotnet run --file Program.cs.

csharp Program.cs
// Program komposisi Task dengan WhenAll dan WhenAny.

async Task<int> HitungHurufAsync(string kata)
{
    await Task.Delay(50);
    return kata.Length;
}

async Task<string> BeriNamaAsync(string nama, int durasiMs)
{
    await Task.Delay(durasiMs);
    return nama;
}

string[] kata = ["kopi", "gula", "teh"];
Task<int>[] tugasHitung =
[
    HitungHurufAsync(kata[0]),
    HitungHurufAsync(kata[1]),
    HitungHurufAsync(kata[2])
];
int[] jumlahHuruf = await Task.WhenAll(tugasHitung);
Console.WriteLine($"Jumlah huruf tiap kata: {string.Join(", ", jumlahHuruf)}");
Console.WriteLine($"Total huruf: {jumlahHuruf.Sum()}");

Task<string> cepat = BeriNamaAsync("tugas cepat", 50);
Task<string> lambat = BeriNamaAsync("tugas lambat", 150);
Task<string> selesaiPertama = await Task.WhenAny(cepat, lambat);
Console.WriteLine($"Selesai lebih dahulu: {await selesaiPertama}");
text Keluaran
Jumlah huruf tiap kata: 4, 4, 3
Total huruf: 11
Selesai lebih dahulu: tugas cepat

Ketiga penghitungan dimulai hampir bersamaan karena setiap pemanggilan HitungHurufAsync tidak langsung di-await, melainkan disimpan sebagai tugas dalam array terlebih dahulu. Await baru ditempatkan pada Task.WhenAll, sehingga waktu tunggunya sekitar durasi tugas yang paling lama, bukan jumlah durasi semuanya. Hasil gabungan berupa array int yang urutannya mengikuti urutan tugas pada argumen WhenAll, bukan urutan tugas yang selesai lebih dulu. Karena itu keluaran 4, 4, 3 selalu cocok dengan urutan kata kopi, gula, dan teh.

Sifat lain yang perlu diingat adalah penanganan error. Apabila salah satu tugas gagal, await atas tugas gabungan melempar exception yang pertama terjadi, sedangkan exception dari tugas lainnya tetap tersimpan dalam objek AggregateException yang melekat pada tugas gabungan itu. Seluruh tugas tetap dijalankan sampai tuntas meskipun salah satunya gagal lebih awal.

Memilih tugas tercepat dengan WhenAny#

Task.WhenAny menerima kumpulan tugas dan mengembalikan tugas yang selesai paling dulu, entah karena berhasil maupun karena gagal. Bagian kedua program di atas membalapkan dua tugas berdurasi berbeda. Tugas berdurasi 50 milidetik unggul atas tugas berdurasi 150 milidetik secara pasti, sehingga keluaran tugas cepat selalu tercetak. Hasil kembalian WhenAny berupa satu tugas, sehingga nilainya diambil dengan await kedua pada pernyataan terakhir program.

Tugas yang kalah lomba tidak dihentikan secara otomatis, melainkan tetap berjalan sampai tuntas di latar belakang. Apabila pekerjaannya sudah tidak diperlukan, tugas tersebut perlu dibatalkan melalui CancellationToken sebagaimana dibahas pada topik Pembatalan. Pola WhenAny juga menjadi dasar teknik batas waktu (timeout), yaitu membalapkan pekerjaan utama dengan Task.Delay, sehingga pemanggil dapat bereaksi apabila pekerjaan utama berlangsung terlalu lama.

Kesalahan umum#

Empat kesalahan berikut paling sering muncul ketika tugas dikomposisikan.

  • Menunggu tugas satu per satu di dalam perulangan. await yang ditempatkan di dalam badan perulangan membuat tugas berikutnya baru dimulai setelah tugas sebelumnya selesai, sehingga pekerjaan berjalan berurutan padahal tujuannya bersamaan. Mulai seluruh tugas terlebih dahulu, lalu gabungkan dengan WhenAll.
  • Mengira hasil WhenAll terurut sesuai waktu selesai. Urutannya selalu mengikuti urutan argumen, sehingga hasil tiap tugas perlu dicocokkan dengan tugas asalnya.
  • Melupakan tugas yang kalah dalam WhenAny. Tugas itu tetap berjalan dan errornya tidak teramati apabila tidak ada yang menunggu, sehingga tugas tersebut perlu dibatalkan atau diamati secara eksplisit.
  • Menyimpulkan tugas gabungan berhasil padahal ada anggota yang gagal. Pemeriksaan error tetap diperlukan karena await atas WhenAll hanya melempar exception pertama.

Ringkasan#

  • Task mewakili pekerjaan asinkron yang belum selesai beserta keadaannya, sedangkan Task<T> membawa satu nilai hasil.
  • Task.WhenAll mengembalikan satu tugas gabungan dengan hasil berupa array yang urutannya mengikuti urutan argumen, sehingga banyak pekerjaan berjalan bersamaan dengan satu titik tunggu.
  • Task.WhenAny mengembalikan tugas yang selesai paling dulu, sedangkan tugas yang kalah tetap berjalan sehingga perlu dibatalkan apabila tidak diperlukan.

Referensi#