Menu

C# async await: Task, Task<T>, WhenAll ve Yaygın Tuzaklar

async ve await, C# kodunun bir thread'i bloklamadan yavaş işlemleri beklemesini sağlar. Bir async metodun nasıl çalıştığını, Task ve Task<T>'yi, Task.WhenAll ile bağımsız işlemleri aynı anda çalıştırmayı, async kodda istisnaları ve async void ile .Result'ın neden hatalara ve deadlock'lara yol açtığını öğrenin.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

Bir programdaki yavaş işlemlerin çoğu beklemedir: bir web sunucusunun cevap vermesini, bir veritabanının satırları döndürmesini, bir dosyanın okunmasını beklemek. async ve await, bir metodun böyle bir bekleme sırasında bir thread'i rehin tutmadan duraklamasını, sonra kaldığı yerden devam etmesini sağlar. Kod yine de sıradan kod gibi yukarıdan aşağıya okunur.

İlk async metot

Bir async metot Task (sonuç yok) ya da Task<T> (T tipinde bir sonuç) döndürür. İçinde await başka bir task'ı bekler ve size sonucunu verir.

Çıktı:

Tea 2.50, cake 4.00, total 6.50

GetPriceAsync return 2.50m der ve derleyici bunu metodun döndürdüğü Task<decimal>'a sarar. await onu tekrar açar. Task.Delay, Thread.Sleep'in async sürümüdür: süre geçtikten sonra tamamlanır ve bu arada bir thread'i bloklamaz.

C# 7.1'den beri Main'in kendisi async olabilir ve modern .NET'te bu programı böyle yazardınız:

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

Bu sayfadaki çalıştırılabilir örnekler, async bir RunAsync metodunu normal bir Main'den .GetAwaiter().GetResult() ile çağırır; derleyicinin async bir Main için ürettiği budur. Bir konsol uygulamasında bu güvenlidir; deadlock bölümü aynı çağrının UI kodunda neden tehlikeli olduğunu açıklar.

Bir await'te ne olur

Bir async metot, bitmemiş bir task üzerindeki ilk await'ine kadar eşzamanlı çalışır. O noktada çağıranına bir Task döndürür ve metodun geri kalanı, beklenen task tamamlandığında çalışan bir devam (continuation) olur.

Çıktı:

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

"Download: starting", DownloadAsync dönmeden önce yazdırılır, çünkü ilk await'e kadar her şey çağıranın thread'inde çalışır. Sonra metot bitmemiş bir task verir, çağıran devam eder ve "Download: finished" ancak gecikme bittiğinde görünür. Bir async metodu çağırmak onu başlatır; await etmek sonucu beklemenin yoludur.

async bir thread oluşturmaz. Metot duraklatılmışken onu bekleyen hiçbir thread yoktur; bir sunucunun yalnızca bir avuç thread'le binlerce isteğin bir veritabanını beklemesini sağlayabilmesinin nedeni budur. Başka bir thread'de çalışması gereken CPU ağırlıklı işler için task'lar sayfasında ele alınan Task.Run'ı kullanın.

Task.WhenAll ile işlemleri aynı anda çalıştırmak

Bir çağrıyı ötekinin ardından await etmek sıralıdır: her biri ancak öncekinin bitmesinden sonra başlar. İşlemler birbirine bağlı değilse hepsini başlatın ve sonra Task.WhenAll ile birlikte await edin:

Örnek çıktı:

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

Sıralı sürüm üç beklemenin toplamı kadar, eşzamanlı olan ise yaklaşık en yavaşı kadar sürer. Task.WhenAll, hangisinin önce bittiğinden bağımsız olarak sonuçları verdiğiniz task'larla aynı sırada döndürür. Bir listeyle de çalışır; "her öğeyi getir" için olağan şekil budur: await Task.WhenAll(ids.Select(id => FetchAsync(id))).

Task.WhenAny, ilk task tamamlanır tamamlanmaz tamamlanan karşılığıdır; zaman aşımları ve "ilk cevap kazanır" için işe yarar.

Async kodda istisnalar

Bir async metodun içinde fırlatılan bir istisna döndürülen task'ta saklanır ve task await edildiğinde yeniden fırlatılır, bu yüzden await'in etrafındaki sıradan bir try/catch çalışır:

Çıktı:

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

LoadProfileAsync(-1) çağırmak istisna fırlatmadı: istisna bir şey onu await edene kadar task'ta yaşar. Kimsenin hiç await etmediği bir task istisnasını tamamen gizler; başlattığınız şeyi her zaman await etmek için bir neden daha. WhenAll ile await ilk istisnayı yeniden fırlatır, birleşik task'ın Exception property'si (bir AggregateException) ise hepsini tutar. Orada ok.Result'ı okumak sorun değildir, çünkü ok zaten tamamlanmıştır.

async void: yalnızca event handler'lar için

Bir async metot void da döndürebilir. Event handler'lar dışında her yerde bundan kaçının:

// 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); }
}

async void ile await edilecek bir task yoktur, bu yüzden çağıran iş bitmeden devam eder ve içinde fırlatılan bir istisna, hiçbir çağıranın try/catch'inin ulaşamayacağı senkronizasyon bağlamında (ya da thread pool'da) doğrudan fırlatılır. Bir konsol ya da sunucu uygulamasında bu genellikle süreci sonlandırır. Async void bir event handler'ın içinde her şeyi kendiniz yakalayın.

İlgili bir hata, async bir metodu await olmadan çağırmaktır. Derleyici uyarır (CS4014) ve metot, sonucunu ya da hatalarını hiçbir şey gözlemlemeden arka planda çalışır.

.Result ve .Wait()'ten gelen deadlock'lar

Bir task üzerinde .Result ya da .Wait() ile bloklamak, async kodun en sık ters gittiği yerdir. Senkronizasyon bağlamı olan bir uygulamada (WinForms, WPF, MAUI, klasik ASP.NET) sıra şöyledir:

  1. UI thread'i GetDataAsync().Result'ı çağırır ve task'ı bekleyerek bloklanır.
  2. GetDataAsync içinde bir await biter. Varsayılan olarak devamı başladığı bağlamda çalışmalıdır: UI thread'inde.
  3. UI thread'i 1. adımda bloklanmıştır, bu yüzden devam hiç çalışmaz, bu yüzden task hiç tamamlanmaz, bu yüzden 1. adım hiç bitmez.

Uygulama hiçbir istisna olmadan donar. Konsol uygulamalarında ve ASP.NET Core'da böyle bir bağlam yoktur; aynı kodun bir test programında çalışıp bir masaüstü uygulamasında takılmasının nedeni budur. Çözümler:

  • Bloklamak yerine çağrı zincirinin sonuna kadar await kullanın ("sonuna kadar async"). Event handler'lar bu amaçla async void olabilir.
  • Çağıranın bağlamına dönmesi gerekmeyen kütüphane kodunda await SomethingAsync().ConfigureAwait(false); yazın. Devam bundan sonra yakalanan bağlam yerine thread pool'da çalışır ve kütüphane gereksiz bir thread geçişini atlar. Yalnızca zincirdeki her await aynısını yaparsa bloklayan bir çağıranı korur, bu yüzden onu .Result için bir çözüm olarak değil, iyi bir kütüphane hijyeni olarak görün.

ASP.NET Core'daki uygulama kodu ConfigureAwait(false)'a ihtiyaç duymaz, çünkü dönülecek bir bağlam yoktur.

Yaygın hatalar

  • Bağımsız çağrıları tek tek await etmek. Onları başlatın, sonra await Task.WhenAll(...).
  • async void metotlar. Task döndürün; async void'i event handler'lar için saklayın.
  • UI ya da klasik ASP.NET kodunda .Result ve .Wait(). Deadlock'a yol açarlar; bunun yerine await edin.
  • await'i unutmak. İş gözlemlenmeden çalışır ve istisnaları kaybolur.
  • G/Ç'yi Task.Run içine sarmak. File.ReadAllTextAsync ya da HttpClient.GetStringAsync gibi async bir API zaten thread'i serbest bırakır; Task.Run yalnızca bir thread pool atlaması ekler.
  • async'in "başka bir thread'de çalışır" anlamına geldiğini varsaymak. "Bloklamadan duraklayabilir" demektir. CPU'ya bağlı iş için Task.Run kullanın.

Sıkça Sorulan Sorular

C#'ta async ve await nasıl çalışır?

Bir metodu async işaretlemek onun await kullanmasını sağlar. Metot bitmemiş bir task üzerinde await'e ulaştığında hemen çağıranına döner ve işin geri kalanını temsil eden bir Task verir. Beklenen işlem tamamlandığında metot await'ten sonra devam eder. Beklerken bloklanmış hiçbir thread yoktur.

Task ile Task<T> arasındaki fark nedir?

Task, değer üretmeyen bir işlemi temsil eder; bir void metodun async sürümüdür. Task<T> bir T üreten bir işlemi temsil eder: bir Task<int> üzerinde await size int'i verir. async Task<int> bildirilmiş bir async metot sadece return 42; yazar ve derleyici onu task'a sarar.

Birden fazla async işlem aynı anda nasıl çalıştırılır?

Önce hepsini başlatın, sonra birlikte await edin: var a = GetUserAsync(); var b = GetOrdersAsync(); await Task.WhenAll(a, b);. await GetUserAsync(); await GetOrdersAsync(); yazmak onları birbiri ardına çalıştırır, bu yüzden toplam süre en uzun olan yerine toplam olur.

C#'ta async void neden kötü?

Bir async void metot await edilemez, bu yüzden çağıran ne zaman bittiğini bilmez ve içinde fırlatılan bir istisna çağıran tarafından yakalanamaz: senkronizasyon bağlamında fırlatılır ve genellikle süreci çökertir. Bunun yerine Task döndürün. async void yalnızca imzası void gerektiren event handler'lar için vardır.

.Result ya da .Wait() neden deadlock'a yol açar?

Senkronizasyon bağlamı olan bir uygulamada (WinForms, WPF, klasik ASP.NET), .Result UI ya da istek thread'ini bloklar; beklenen metodun devamı ise aynı thread'de çalışmayı bekler. Her biri sonsuza dek diğerini bekler. Bloklamak yerine sonuna kadar await kullanın ve kütüphane kodunun içinde ConfigureAwait(false) kullanın.

C#'ta Main async olabilir mi?

Evet, C# 7.1'den beri: static async Task Main() ya da static async Task<int> Main(). Top-level statements (C# 9) doğrudan await kullanabilir. Eski kodda karşılığı normal bir Main'den MainAsync().GetAwaiter().GetResult() çağırmaktır; bu orada güvenlidir, çünkü bir konsol uygulamasının senkronizasyon bağlamı yoktur.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA