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:
- UI thread'i
GetDataAsync().Result'ı çağırır ve task'ı bekleyerek bloklanır. GetDataAsynciçinde birawaitbiter. Varsayılan olarak devamı başladığı bağlamda çalışmalıdır: UI thread'inde.- 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
awaitkullanın ("sonuna kadar async"). Event handler'lar bu amaçlaasync voidolabilir. - Ç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 herawaitaynısını yaparsa bloklayan bir çağıranı korur, bu yüzden onu.Resultiç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 voidmetotlar.Taskdöndürün;async void'i event handler'lar için saklayın.- UI ya da klasik ASP.NET kodunda
.Resultve.Wait(). Deadlock'a yol açarlar; bunun yerine await edin. await'i unutmak. İş gözlemlenmeden çalışır ve istisnaları kaybolur.- G/Ç'yi
Task.Runiçine sarmak.File.ReadAllTextAsyncya daHttpClient.GetStringAsyncgibi async bir API zaten thread'i serbest bırakır;Task.Runyalnı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çinTask.Runkullanı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.