Menu

async await C#: Task, Task<T>, WhenAll, dan Jebakan Umum

async dan await membuat kode C# bisa menunggu operasi yang lambat tanpa memblokir thread. Pelajari cara method async berjalan, Task dan Task<T>, menjalankan operasi yang saling lepas secara bersamaan dengan Task.WhenAll, exception di kode async, dan kenapa async void dan .Result menyebabkan bug dan deadlock.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Sebagian besar operasi lambat dalam sebuah program sebenarnya adalah menunggu: menunggu server web menjawab, database mengembalikan baris, atau file selesai dibaca. async dan await membuat sebuah method bisa berhenti sejenak selama menunggu tanpa menyandera sebuah thread, lalu melanjutkan dari titik terakhirnya. Kodenya tetap terbaca dari atas ke bawah seperti kode biasa.

Method async pertama

Method async mengembalikan Task (tanpa hasil) atau Task<T> (hasil bertipe T). Di dalamnya, await menunggu task lain dan memberimu hasilnya.

Output:

Tea 2.50, cake 4.00, total 6.50

GetPriceAsync menulis return 2.50m, dan compiler membungkusnya ke dalam Task<decimal> yang dikembalikan method itu. await membukanya lagi. Task.Delay adalah versi async dari Thread.Sleep: task ini selesai setelah waktunya lewat, tanpa memblokir thread selama menunggu.

Sejak C# 7.1, Main sendiri bisa async, dan begitulah cara kamu menulis program ini di .NET modern:

static async Task Main()
{
    decimal tea = await GetPriceAsync("tea");
    Console.WriteLine(tea);
}

Contoh yang bisa dijalankan di halaman ini memanggil method async RunAsync dari Main biasa dengan .GetAwaiter().GetResult(), yang sama dengan kode yang dihasilkan compiler untuk Main async. Di aplikasi console, cara ini aman; bagian tentang deadlock menjelaskan kenapa panggilan yang sama berbahaya di kode UI.

Apa yang terjadi saat await

Method async berjalan secara sinkron sampai await pertamanya pada task yang belum selesai. Di titik itu method mengembalikan sebuah Task ke pemanggilnya, dan sisa method menjadi lanjutan (continuation) yang berjalan saat task yang ditunggu selesai.

Output:

Calling DownloadAsync
  Download: starting
DownloadAsync returned a task; doing other work
  Download: finished
Download awaited

"Download: starting" tercetak sebelum DownloadAsync kembali, karena semua kode sampai await pertama berjalan di thread pemanggil. Lalu method menyerahkan task yang belum selesai, pemanggil terus berjalan, dan "Download: finished" baru muncul saat delay-nya selesai. Memanggil method async berarti memulainya; meng-await-nya adalah cara kamu menunggu hasilnya.

async tidak membuat thread. Selama method berhenti sejenak, tidak ada satu thread pun yang menunggunya, itulah sebabnya sebuah server bisa punya ribuan request yang menunggu database hanya dengan segelintir thread. Untuk pekerjaan berat di CPU yang perlu berjalan di thread lain, pakai Task.Run, yang dibahas di task.

Menjalankan operasi bersamaan dengan Task.WhenAll

Meng-await panggilan satu demi satu berarti berurutan: setiap panggilan baru dimulai setelah panggilan sebelumnya selesai. Saat operasinya tidak saling bergantung, mulai semuanya lalu tunggu bersama-sama dengan Task.WhenAll:

Contoh output:

Sequential: 160 units in ~900 ms
Concurrent: 160 units in ~300 ms

Versi berurutan butuh waktu sebanyak jumlah ketiga waktu tunggunya, sedangkan versi bersamaan kira-kira selama yang paling lambat. Task.WhenAll mengembalikan hasil dengan urutan yang sama seperti task yang kamu berikan, tidak peduli mana yang selesai lebih dulu. Method ini juga bisa dipakai dengan list, yang merupakan bentuk umum untuk "ambil setiap item": await Task.WhenAll(ids.Select(id => FetchAsync(id))).

Task.WhenAny adalah pasangannya yang selesai begitu task pertama selesai, berguna untuk timeout dan "jawaban pertama yang menang".

Exception di kode async

Exception yang dilempar di dalam method async disimpan di task yang dikembalikan dan dilempar ulang saat task itu di-await, jadi try/catch biasa di sekitar await tetap berfungsi:

Output:

Task created, completed yet: False
Caught ArgumentOutOfRangeException for userId
WhenAll rethrew the first failure
All failures: 1
The other task still succeeded: profile 7

Memanggil LoadProfileAsync(-1) tidak melempar exception: exception-nya tersimpan di task sampai ada yang meng-await-nya. Task yang tidak pernah di-await menyembunyikan exception-nya sepenuhnya, dan ini satu alasan lagi untuk selalu meng-await apa yang kamu mulai. Dengan WhenAll, await melempar ulang exception pertama, sedangkan property Exception dari task gabungannya (sebuah AggregateException) menyimpan semuanya. Membaca ok.Result di sana aman karena ok sudah selesai.

async void: hanya untuk event handler

Method async juga bisa mengembalikan void. Hindari itu di mana pun kecuali di event handler:

// Bad: the caller cannot await it or catch its exceptions.
static async void SaveAsync(Order order)
{
    await db.InsertAsync(order);   // if this throws, the process may crash
}

// Good: return Task, so callers can await and handle errors.
static async Task SaveAsync(Order order)
{
    await db.InsertAsync(order);
}

// Acceptable: an event handler must return void.
private async void SaveButton_Click(object sender, EventArgs e)
{
    try { await SaveAsync(currentOrder); }
    catch (Exception ex) { ShowError(ex); }
}

Dengan async void, tidak ada task yang bisa di-await, jadi pemanggil terus berjalan sebelum pekerjaannya selesai, dan exception yang dilempar di dalamnya dimunculkan langsung di synchronization context (atau thread pool), tempat yang tidak bisa dijangkau try/catch milik pemanggil mana pun. Di aplikasi console atau server, hal ini biasanya menghentikan proses. Di dalam event handler async void, tangkap semua exception sendiri.

Kesalahan yang mirip adalah memanggil method async tanpa await. Compiler memberi warning (CS4014), dan method itu berjalan di latar belakang tanpa ada yang mengamati hasil atau error-nya.

Deadlock karena .Result dan .Wait()

Memblokir task dengan .Result atau .Wait() adalah tempat kode async paling sering salah. Di aplikasi yang punya synchronization context (WinForms, WPF, MAUI, ASP.NET klasik), urutannya seperti ini:

  1. Thread UI memanggil GetDataAsync().Result dan terblokir, menunggu task-nya.
  2. Di dalam GetDataAsync, sebuah await selesai. Secara default, lanjutannya harus berjalan di context tempat ia dimulai: thread UI.
  3. Thread UI sedang terblokir di langkah 1, jadi lanjutannya tidak pernah berjalan, jadi task-nya tidak pernah selesai, jadi langkah 1 tidak pernah berakhir.

Aplikasinya membeku tanpa exception apa pun. Aplikasi console dan ASP.NET Core tidak punya context seperti itu, itulah sebabnya kode yang sama berjalan di program uji coba tetapi macet di aplikasi desktop. Cara mengatasinya:

  • Pakai await di sepanjang rantai pemanggilan alih-alih memblokir ("async sampai ke atas"). Event handler boleh async void untuk keperluan ini.
  • Di kode library yang tidak perlu kembali ke context pemanggil, tulis await SomethingAsync().ConfigureAwait(false);. Lanjutannya lalu berjalan di thread pool, bukan di context yang ditangkap, dan library melewati perpindahan thread yang tidak perlu. Cara ini hanya melindungi pemanggil yang memblokir kalau setiap await di rantainya melakukan hal yang sama, jadi anggap ini kebiasaan baik untuk library, bukan solusi untuk .Result.

Kode aplikasi di ASP.NET Core tidak butuh ConfigureAwait(false), karena tidak ada context untuk kembali.

Kesalahan umum

  • Meng-await panggilan yang saling lepas satu per satu. Mulai semuanya, lalu await Task.WhenAll(...).
  • Method async void. Kembalikan Task; simpan async void untuk event handler.
  • .Result dan .Wait() di kode UI atau ASP.NET klasik. Keduanya menyebabkan deadlock; pakai await.
  • Lupa await. Pekerjaannya berjalan tanpa diamati dan exception-nya hilang.
  • Membungkus I/O dengan Task.Run. API async seperti File.ReadAllTextAsync atau HttpClient.GetStringAsync sudah membebaskan thread-nya; Task.Run hanya menambah satu lompatan ke thread pool.
  • Mengira async berarti "berjalan di thread lain". Artinya "bisa berhenti sejenak tanpa memblokir". Pakai Task.Run untuk pekerjaan yang berat di CPU.

Pertanyaan yang sering diajukan

Bagaimana async dan await bekerja di C#?

Menandai method dengan async membuatnya bisa memakai await. Saat method sampai pada await untuk task yang belum selesai, method langsung kembali ke pemanggilnya sambil menyerahkan sebuah Task yang mewakili sisa pekerjaannya. Saat operasi yang ditunggu selesai, method melanjutkan dari setelah await. Tidak ada thread yang diam terblokir selama menunggu.

Apa bedanya Task dan Task<T>?

Task mewakili operasi yang tidak menghasilkan nilai, versi async dari method void. Task<T> mewakili operasi yang menghasilkan T: await pada Task<int> memberimu int-nya. Method async yang dideklarasikan async Task<int> cukup menulis return 42;, dan compiler membungkusnya ke dalam task.

Bagaimana cara menjalankan beberapa operasi async sekaligus?

Mulai semuanya dulu, lalu tunggu bersama-sama: var a = GetUserAsync(); var b = GetOrdersAsync(); await Task.WhenAll(a, b);. Menulis await GetUserAsync(); await GetOrdersAsync(); menjalankannya satu per satu, jadi total waktunya adalah jumlah keduanya, bukan yang paling lama saja.

Kenapa async void buruk di C#?

Method async void tidak bisa di-await, jadi pemanggil tidak tahu kapan method itu selesai, dan exception yang dilempar di dalamnya tidak bisa ditangkap oleh pemanggil: exception itu dimunculkan di synchronization context dan biasanya membuat proses crash. Kembalikan Task sebagai gantinya. async void hanya ada untuk event handler, yang signature-nya mengharuskan void.

Kenapa .Result atau .Wait() menyebabkan deadlock?

Di aplikasi yang punya synchronization context (WinForms, WPF, ASP.NET klasik), .Result memblokir thread UI atau thread request, sementara lanjutan dari method yang di-await menunggu untuk dijalankan di thread yang sama. Keduanya saling menunggu selamanya. Pakai await di sepanjang rantai pemanggilan alih-alih memblokir, dan pakai ConfigureAwait(false) di dalam kode library.

Apakah Main bisa async di C#?

Bisa, sejak C# 7.1: static async Task Main() atau static async Task<int> Main(). Top-level statement (C# 9) bisa memakai await secara langsung. Di kode lama, padanannya adalah memanggil MainAsync().GetAwaiter().GetResult() dari Main biasa, yang aman di sana karena aplikasi console tidak punya synchronization context.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai