Kebuntuan (deadlock) asinkron paling sering muncul bukan di console, melainkan di aplikasi berantarmuka. Polanya: thread UI memanggil method asinkron lalu memblokir diri dengan Result atau Wait() untuk menunggu hasil. Di dalam task, kelanjutan setelah await berusaha kembali ke konteks UI yang sudah terpasang. Sayangnya thread UI sedang tidur menunggu task, sementara task menunggu thread UI menyediakan gilirannya; keduanya saling menunggu dan program membeku.

Urutan Kejadian Kebuntuan #

Kebuntuan pada aplikasi UI berjalan melalui empat langkah yang saling menunggu:

  1. Thread UI memanggil method asinkron lalu segera memblokir diri pada .Result atau Wait().
  2. Task di dalam method berjalan sampai await pertama, kemudian mengembalikan kendali kepada pemanggil.
  3. Kelanjutan setelah await dijadwalkan kembali ke konteks UI, karena konteks itu aktif pada titik await diambil.
  4. Thread UI menunggu task selesai, sementara task menunggu thread UI bebas; keduanya tidak bergerak dan antarmuka berhenti merespons.

Blok ConfigureAwait(false) di dalam pustaka memutus langkah ketiga, karena kelanjutan tidak diminta kembali ke konteks UI. Namun langkah paling aman tetap sama: jangan memblokir thread pemanggil sama sekali.

Pola Berbahaya dan Pola Aman #

Kode berikut memperlihatkan pola berbahaya pada aplikasi UI; pola ini tidak dijalankan di console karena console tidak memiliki SynchronizationContext sehingga kebuntuan tidak terjadi di sana:

csharp
{title="Pola berbahaya di aplikasi UI (tidak dijalankan)"}
private async void TombolHitung_Click(object sender, EventArgs e)
{
    int total = HitungTotalAsync().Result;   // thread UI memblokir diri
    labelTotal.Text = total.ToString();
}

Ketika HitungTotalAsync mencapai await pertamanya, kelanjutannya dijadwalkan kembali ke konteks UI. Thread UI pada saat itu sedang tertahan di Result, sehingga kelanjutan tidak pernah berjalan. Pola amannya sederhana: buat metode event handler async void dan await sampai ke lapisan terbawah, seperti pada contoh yang dijalankan berikut:

csharp
static async Task<int> AmbilHargaTokoAsync()
{
    await Task.Delay(40);
    return 35000;
}

static async Task<int> HitungTotalAsync()
{
    int harga = await AmbilHargaTokoAsync();
    return harga * 2;
}

// Pemanggil:
int total = await HitungTotalAsync();
Console.WriteLine($"Pola aman (await sampai ke atas): Rp{total}");

int nilai = Task.Run(() => 7 * 6000).Result;
Console.WriteLine($"Pemblokiran kecil di console: Rp{nilai}");

Output yang Dihasilkan:

text
Pola aman (await sampai ke atas): Rp70000
Pemblokiran kecil di console: Rp42000

Mari kita bedah keduanya:

  1. await HitungTotalAsync(): seluruh rantai ditunggu tanpa memblokir thread; hasil 70000 muncul setelah dua lapis await selesai.
  2. Task.Run(() => 7 * 6000).Result: pemblokiran ini berjalan di console tanpa kebuntuan karena tidak ada konteks yang harus dikembaliki; pada aplikasi UI pola identik dapat membekukan antarmuka.

Pemahaman jebakan ini menjadi dasar daftar praktik terbaik pada bab berikutnya.