
Foto oleh Ann H via Pexels
Pemrograman asynchronous memungkinkan program menjalankan operasi yang memakan waktu lama, seperti memanggil API eksternal atau membaca file besar, tanpa memblokir thread utama yang bisa membuat aplikasi terasa 'hang' atau tidak responsif selama operasi tersebut berlangsung.
C# menyediakan kata kunci async dan await untuk menulis kode asynchronous dengan gaya yang terlihat seperti kode synchronous biasa, jauh lebih mudah dibaca dibanding pendekatan callback bersarang yang umum ditemui pada bahasa lain sebelum pola async-await populer.
Artikel ini membahas cara menulis method asynchronous dengan Task dan Task
Method Asynchronous Dasar dengan Task
Method async mengembalikan Task atau Task<T>
Menjalankan Beberapa Task Secara Bersamaan
Task.WhenAll menjalankan beberapa task secara paralel
Bahaya async void Kecuali untuk Event Handler
async void membuat exception tidak bisa ditangkap dengan benar
ConfigureAwait untuk Konteks Eksekusi
ConfigureAwait(false) menghindari deadlock pada kode library
Menghindari Deadlock dengan ConfigureAwait dan Async All The Way
ConfigureAwait(false) dan risiko memanggil .Result pada kode async
Prinsip async all the way menyatakan bahwa begitu sebuah method menggunakan await di dalamnya, method pemanggilnya sebaiknya juga dibuat async dan memanggilnya dengan await, bukan dengan memaksa method tersebut berjalan synchronous melalui properti .Result atau method .Wait(). Pada aplikasi dengan SynchronizationContext tertentu seperti WPF, WinForms, atau ASP.NET (non-Core) klasik, pemanggilan .Result pada method async dapat menyebabkan deadlock karena thread utama menunggu hasil Task sementara Task tersebut menunggu giliran kembali ke thread utama yang sedang diblokir.
ConfigureAwait(false) memberi tahu runtime bahwa kode setelah await tidak perlu dilanjutkan pada SynchronizationContext awal, melainkan boleh dilanjutkan pada thread pool mana saja yang tersedia, sehingga mengurangi overhead context-switching dan menghindari risiko deadlock yang disebutkan sebelumnya. Praktik ini umum diterapkan pada kode library yang tidak perlu mengakses elemen UI setelah operasi asynchronous selesai.
Pada aplikasi ASP.NET Core modern, risiko deadlock akibat .Result jauh berkurang karena ASP.NET Core tidak lagi menggunakan SynchronizationContext seperti versi sebelumnya, namun kebiasaan menggunakan await secara konsisten tetap direkomendasikan karena tetap memblokir thread pool secara tidak perlu dan mengurangi skalabilitas aplikasi dalam menangani request secara bersamaan.
Task.WhenAll dan Task.WhenAny untuk Operasi Asynchronous Paralel
Task.WhenAll menunggu semua task selesai dan berjalan bersamaan, bukan berurutan
Task.WhenAll menerima beberapa Task sekaligus dan mengembalikan satu Task baru yang baru selesai (completed) ketika seluruh Task yang diberikan telah selesai dijalankan, memungkinkan beberapa operasi asynchronous dijalankan secara bersamaan (concurrent) alih-alih berurutan satu per satu menggunakan await terpisah. Pada contoh di atas, ketiga pemanggilan API berjalan paralel sehingga total waktu tunggu mendekati durasi Task terlama saja, yaitu sekitar 300 milidetik, bukan penjumlahan seluruh delay yang mencapai 600 milidetik.
Task.WhenAny berbeda dari Task.WhenAll karena hanya menunggu Task pertama yang selesai di antara seluruh Task yang diberikan, lalu langsung mengembalikan referensi ke Task tersebut tanpa menunggu Task lainnya selesai. Pola ini berguna untuk skenario seperti memanggil beberapa endpoint API cadangan secara bersamaan dan cukup memakai hasil dari server mana pun yang merespons paling cepat, sambil Task lainnya tetap berjalan di latar belakang atau dibatalkan menggunakan CancellationToken.
Penting diperhatikan bahwa hasil dari Task.WhenAll berupa array yang urutannya mengikuti urutan Task yang diberikan sebagai argumen, bukan urutan selesainya masing-masing Task, sehingga hasil task2 yang selesai paling cepat tetap berada di index kedua pada array hasil akhir. Jika salah satu Task di dalam Task.WhenAll melempar exception, exception tersebut akan ditangkap dan dilempar ulang setelah seluruh Task selesai, bukan langsung menghentikan Task lain yang masih berjalan. Jika lebih dari satu Task melempar exception secara bersamaan, seluruh exception tersebut akan digabung ke dalam satu AggregateException yang bisa diperiksa satu per satu melalui property InnerExceptions untuk mengetahui penyebab kegagalan masing-masing Task.
IAsyncEnumerable dan await foreach untuk Streaming Data Asynchronous
IAsyncEnumerable
Konsumsi data dari IAsyncEnumerable dilakukan dengan await foreach, yang akan menunggu setiap elemen tersedia satu per satu tanpa memblokir thread utama selama proses menunggu berlangsung di antara setiap iterasi. Pola ini cocok untuk skenario streaming seperti membaca hasil query besar secara potongan demi potongan, menerima data real-time dari sensor, atau memproses respons API yang datang bertahap tanpa harus menunggu seluruh data terkumpul lebih dulu di memori.
Kesalahan Umum yang Sering Terjadi
Memakai async void untuk Method Selain Event Handler
async void membuat exception sulit ditangkap dan proses sulit ditunggu selesainya oleh pemanggil, sehingga sebaiknya selalu memakai async Task bahkan ketika method tidak mengembalikan nilai apa pun.
Memanggil .Result atau .Wait() pada Task
Memanggil .Result atau .Wait() secara langsung pada Task alih-alih memakai await bisa menyebabkan deadlock pada aplikasi dengan konteks sinkronisasi tertentu, seperti aplikasi desktop WPF atau ASP.NET klasik, meski dampaknya lebih jarang terjadi pada ASP.NET Core modern.
Menjalankan Task Secara Berurutan Padahal Bisa Paralel
Memanggil beberapa operasi asynchronous independen satu per satu dengan await terpisah padahal tidak saling bergantung akan membuang waktu yang sebenarnya bisa dihemat dengan menjalankannya bersamaan lewat Task.WhenAll.
Kapan Pendekatan Ini Tidak Cocok Dipakai
Pemrograman asynchronous dengan async-await memberikan responsivitas aplikasi yang jauh lebih baik, terutama untuk operasi I/O seperti pemanggilan API atau akses database, namun menambah kompleksitas kode dan membutuhkan pemahaman yang tepat tentang bagaimana Task bekerja di balik layar.
Menjalankan task secara paralel dengan Task.WhenAll mempercepat total waktu eksekusi, namun perlu diperhatikan apakah resource yang diakses (seperti koneksi database terbatas) cukup untuk menangani beberapa operasi bersamaan tanpa menimbulkan masalah baru seperti connection pool yang habis.
Ringkasan
Async-await memungkinkan penulisan kode asynchronous yang mudah dibaca layaknya kode synchronous biasa, dengan Task sebagai representasi operasi yang sedang berjalan. Hindari async void kecuali untuk event handler, manfaatkan Task.WhenAll untuk operasi paralel, dan selalu gunakan await alih-alih .Result atau .Wait() untuk menghindari potensi deadlock.
Seri Belajar C# Fundamental:
← Sebelumnya: Delegate Event dan Action Func di C#
→ Selanjutnya: Nullable Type dan Null Safety di C#