Praktik terbaik async berkumpul pada satu prinsip, yaitu async menyeluruh (async all the way), yang berarti pekerjaan asinkron di-await dari lapisan terdalam sampai titik masuk tanpa diblokir di tengah jalan. Topik ini membahas penerapan prinsip tersebut, penggunaan Task.Run untuk kerja CPU, dan cara menjauhi deadlock yang lahir dari pencampuran kode blokir dengan kode asinkron. Setelah membacanya, rantai panggilan asinkron dapat ditulis secara konsisten dan jebakan pemblokirannya dikenali sejak awal.
Prinsip async menyeluruh#
Metode yang memanggil metode async wajib menunggu hasilnya, dan cara menunggu itulah yang menentukan kesehatan program. Await membuat thread bebas selama menunggu, sedangkan pemanggilan blokir seperti properti Result menahan thread sampai pekerjaan tuntas. Prinsip async menyeluruh meminta setiap lapisan memilih await, sehingga tidak ada lapisan yang memblokir di antara rantai panggilan.
Program berikut memuat rantai tiga metode, yaitu metode titik masuk, metode perantara, dan sumber data. Salin programnya ke berkas Program.cs, lalu jalankan dengan perintah dotnet run --file Program.cs.
// Program praktik async: rantai async menyeluruh dan pemindahan kerja CPU.
async Task<string> AmbilIsiLaporanAsync()
{
await Task.Delay(50);
return "isi laporan cuaca";
}
async Task<int> HitungKarakterAsync()
{
string isi = await AmbilIsiLaporanAsync();
return isi.Length;
}
async Task JalankanAsync()
{
int jumlahKarakter = await HitungKarakterAsync();
Console.WriteLine($"Jumlah karakter laporan: {jumlahKarakter}");
}
await JalankanAsync();
Console.WriteLine("Perhitungan berat dikirim ke thread lain");
int total = await Task.Run(() =>
{
int akumulasi = 0;
for (int i = 1; i <= 100; i++)
{
akumulasi += i;
}
return akumulasi;
});
Console.WriteLine($"Hasil perhitungan: {total}");Jumlah karakter laporan: 17
Perhitungan berat dikirim ke thread lain
Hasil perhitungan: 5050Ketiga metode menandai dirinya async dan mengembalikan Task, sehingga keasinkronan mengalir dari AmbilIsiLaporanAsync sampai pernyataan tingkat atas. Tidak ada satu pun lapisan yang memblokir, dan keluarannya tercetak dalam urutan yang pasti karena tiap lapisan menunggu lapisan di bawahnya dengan await. Apabila suatu lapisan tidak punya pekerjaan selain meneruskan panggilan, metode itu boleh mengembalikan Task dari metode bawahnya secara langsung tanpa penanda async, tetapi tanda tangan Task-nya tetap dipertahankan.
Exception pada rantai async bergerak ke atas melalui Task yang sama dengan hasilnya. Metode terdalam yang melempar exception membuat Task-nya berstatus gagal, lalu await pada lapisan atasnya melempar ulang exception yang sama, dan begitu seterusnya sampai titik yang menanganinya. Karena itu satu blok try-catch pada titik masuk cukup menangkap error dari lapisan mana pun, dan alur errornya tidak berbeda dari kode serempak.
Memindahkan kerja CPU dengan Task.Run#
Bagian kedua program memindahkan kerja hitung ke thread pool dengan Task.Run. Metode ini menerima delegate lalu menjalankannya pada thread pekerja, sehingga thread pemanggil tetap bebas, misalnya untuk merespons antarmuka. Hasilnya berupa Task yang di-await seperti hasil metode async lainnya, dan pada program di atas hasil 5050 baru tercetak setelah perhitungan selesai.
Task.Run tepat untuk kerja CPU-bound pada aplikasi yang harus tetap responsif, misalnya merangkum ribuan baris teks sementara pengguna masih mengetik. Task.Run tidak tepat untuk membungkus pekerjaan I/O yang sudah punya versi async, karena lapisan tambahan itu hanya memindahkan penantian tanpa menghemat apa pun. Pustaka yang baik menyediakan metode async asli untuk I/O dan menyisakan keputusan Task.Run kepada pemanggilnya.
Menjauhi deadlock#
Deadlock pada kode asinkron hampir selalu lahir dari pemanggilan blokir atas Task di lingkungan yang punya synchronization context, yaitu mekanisme yang menentukan thread tempat kelanjutan dijalankan. Aplikasi antarmuka grafis seperti Windows Forms dan WPF memiliki satu synchronization context pada thread antarmuka, sedangkan aplikasi konsol tidak memilikinya. Perbedaan inilah yang membuat pola buruk terlihat berjalan pada konsol tetapi membeku pada aplikasi grafis.
// Contoh: akses hasil dengan Result pada aplikasi konsol.
async Task<int> HitungAsync()
{
await Task.Delay(50);
return 42;
}
int hasil = HitungAsync().Result;
Console.WriteLine($"Hasil: {hasil}");Hasil: 42Program konsol di atas berhasil mencetak Hasil 42 karena tanpa synchronization context, kelanjutan HitungAsync dijalankan pada thread pool, bukan pada thread yang sedang diblokir. Keberhasilan ini menyesatkan. Pola yang sama pada aplikasi grafis membentuk lingkaran menunggu, yaitu thread antarmuka menunggu Task melalui Result, sementara kelanjutan Task menunggu thread antarmuka yang sedang sibuk menunggu. Keduanya saling menunggu selamanya.
// tidak-dijalankan: deadlock hanya terjadi pada lingkungan ber synchronization context seperti aplikasi UI.
private async void TombolHitung_Click(object pengirim, EventArgs e)
{
int hasil = HitungAsync().Result; // thread antarmuka diblokir di sini
labelHasil.Text = $"Hasil: {hasil}";
}
private async Task<int> HitungAsync()
{
// kelanjutan setelah await ingin kembali ke thread antarmuka yang sedang diblokir
await Task.Delay(100);
return 42;
}Penawarnya konsisten dengan prinsip async menyeluruh. Ganti Result dengan await, ganti Wait dengan await, dan biarkan tanda tangan metode berupa Task sampai ke titik masuk. Pada kerangka kerja grafis, penangan event bertipe async void memang tidak terhindarkan, tetapi badannya tetap hanya berisi await dan tidak pernah memblokir.
Kode pustaka yang ditulis untuk beragam pemanggil menambahkan satu pengaman lagi, yaitu ConfigureAwait(false) pada setiap await. Angka false menyatakan kelanjutan tidak perlu kembali ke synchronization context pemanggil, sehingga pustaka tidak ikut membentuk lingkaran menunggu meskipun pemanggilnya memblokir. Pada aplikasi antarmuka grafis, pengaturan ini justru tidak dipasang karena kelanjutannya memang harus kembali ke thread antarmuka untuk menyentuh kontrol.
Kesalahan umum#
Empat kesalahan berikut paling sering muncul ketika kode asinkron dipraktikkan.
- Memblokir Task dengan
ResultatauWait. Pola ini menahan sebuah thread dan berisiko deadlock pada lingkungan ber synchronization context, sehingga pemanggilan semacam ini selalu diganti dengan await. - Menulis metode
async voiddi luar event handler. Error dari metode itu tidak dapat ditangkap pemanggilnya, sehingga Task dipilih sebagai tipe kembalian. - Melepaskan Task tanpa penanganan (fire-and-forget). Tugas yang tidak ditunggu menyembunyikan errornya, sehingga pekerjaan penting disimpan ke variabel lalu ditunggu beserta penanganan pembatalannya.
- Membungkus I/O async dengan Task.Run di dalam pustaka. Pembungkus itu menciptakan keasinkronan palsu, karena thread pool tetap menahan pekerjaan yang sebenarnya hanya menunggu. Metode async aslinya yang diekspos.
Ringkasan#
- Async menyeluruh berarti setiap lapisan menunggu dengan await, sehingga tidak ada thread yang diblokir di tengah rantai panggilan.
- Task.Run memindahkan kerja CPU ke thread pool, sedangkan pekerjaan I/O langsung memakai metode async aslinya.
ResultdanWaitadalah sumber deadlock pada lingkungan ber synchronization context, dan perbedaan perilaku antarlingkungan membuat pola itu terlihat aman padahal tidak.
Referensi#
- Asynchronous programming — Microsoft Learn — Panduan pemrograman asinkron C# yang memuat pola pemakaian await sehari-hari.
- Task-based Asynchronous Pattern (TAP) — Microsoft Learn — Spesifikasi pola Task-based yang menetapkan konvensi metode async di .NET.