Bölüm 8 / 13
async/await
Task, senkron/asenkron farkı, deadlock tuzakları, CancellationToken.
13 dakikalık okuma
Hat 02'nin 3. dersi thread'in pahalı bir kaynak olduğunu,
5. dersi de aynı anda çalışan işlerin nasıl birbirine girdiğini anlatmıştı; 4. ders
(exception) AggregateException'ın adını, 7. ders (delegate) ise
async void event handler tuzağının adını buraya bırakmıştı. Bu ders
hepsinin karşılığını ödüyor. Konu, modern .NET kodunun her satırına sinen tek bir
fikirdir: beklerken thread tutma. Mülakatta C#'ın en çok sorulan başlığıdır
ve yanlış cevapların en pahalıya patladığı başlıktır — kilitlenen istekler, tükenen
thread havuzları ve sessizce yutulan hatalar hep bu dersin konusudur.
01Ne öğreneceksin
- Asenkronluğun amacı: neden hız değil ölçeklenebilirlik; I/O-bound ile CPU-bound ayrımı
Tasknedir,async/await'i derleyici neye çevirirawaitsonrası kodun nerede koştuğu: bağlam (context) veConfigureAwait(false)- İki klasik tuzak:
.Result/.Wait()deadlock'u veasync void WhenAll/WhenAnyile birlikte bekleme, hata toplama,AggregateExceptionCancellationTokenile işbirlikçi iptal, zaman aşımı ve saha kararları
02Problem: beklerken tutulan thread
Şu satırı düşün: var veri = File.ReadAllText("rapor.csv"); Disk
veriyi hazırlarken CPU'nun bu iş için yapacağı hiçbir şey yoktur; ama satırı
çalıştıran thread orada durur, bekler ve hiçbir işe yaramadan bellek (kendi
stack'i, tipik olarak 1 MB) ve zamanlayıcı kaydı tutmaya devam eder. Hat 02'nin
3. dersinde thread'in ucuz olmadığını, 4. dersinde her thread'in kendi stack'i
olduğunu görmüştün. Asenkron programlama tam olarak bu israfı hedefler.
Etkiyi görmek için bir web sunucusu düşün. .NET'in thread havuzu (thread pool) sınırlıdır ve talep arttığında yavaş büyür. Her istek veritabanından 100 ms bekliyorsa ve her bekleme bir thread'i tutuyorsa, eşzamanlı istek sayısı havuz kapasitesini aştığı anda sunucu kilitlenmez ama kuyruğa girer: yanıt süreleri birden tırmanır. Asenkron sürümde bekleyen istek hiçbir thread tutmaz — aynı donanım kat kat fazla eşzamanlı isteği taşır.
Buradan çıkan cümle, mülakatın ayırt edici cümlesidir:
- async, işi hızlandırmaz. Tek bir kullanıcı için asenkron sürüm senkron sürümden hızlı değildir; durum makinesi ek yükü yüzünden mikroskobik olarak daha yavaştır bile. Kazandırdığı şey ölçeklenebilirliktir: aynı anda daha çok iş.
- async, thread yaratmaz. Yaygın yanılgı budur. Asenkron bir I/O çağrısı beklerken hiçbir thread harcanmaz; işletim sistemi iş bitince haber verir (Hat 02'nin 2. dersindeki kesme mekanizması), devamı havuzdan bir thread alır.
- Ayrım I/O-bound / CPU-bound'dur. Disk, ağ, veritabanı, HTTP =
I/O-bound →
async/await'in alanı. Sıkıştırma, görüntü işleme, büyük hesap = CPU-bound → burada bekleme yok, iş var; çözüm başka bir thread'e taşımak (Task.Run, paralel işleme) ya da algoritmayı iyileştirmektir.
Masaüstü/mobil tarafında aynı mekanizmanın farklı bir yüzü vardır: orada tek bir UI thread'i vardır ve o thread bloke olursa arayüz donar. "Uygulama yanıt vermiyor" penceresinin arkasındaki sebep, UI thread'inde yapılan senkron beklemedir.
03Task ve derleyicinin ürettiği durum makinesi
Task, "gelecekte tamamlanacak bir iş"i temsil eden nesnedir;
Task<T> ayrıca bir sonuç taşır. Bir Task'ın durumu
tamamlandı (RanToCompletion), hata verdi (Faulted) ya da iptal edildi
(Canceled) olabilir. Kritik ayrım: Task bir thread değildir. Bir
Task, çalışan bir thread'i temsil edebileceği gibi (CPU-bound iş), hiçbir thread
tutmayan bir bekleyişi de temsil edebilir (I/O).
public async Task<string> RaporGetirAsync(int id)
{
using var istemci = new HttpClient();
string govde = await istemci.GetStringAsync($"https://ornek/rapor/{id}"); // I/O: thread yok
return govde.Trim();
}
async anahtar kelimesi metodun imzasını değiştirmez; derleyiciye
metodu bir durum makinesine (state machine) çevir der. Derleyici, her
await noktasını bir "durak" sayar: metodu duraklara bölen gizli bir
sınıf üretir, yerel değişkenleri o sınıfın alanlarına taşır (yani heap'e — 7.
dersteki closure ile aynı mekanizma) ve beklenen iş bitince kaldığı duraktan devam
etmesini sağlar.
Dönüş tipleri ve anlamları:
| Dönüş tipi | Anlamı | Ne zaman |
|---|---|---|
Task | Sonuç taşımayan asenkron iş | Varsayılan; void'in asenkron karşılığı |
Task<T> | T döndüren asenkron iş | Varsayılan; değer döndüren metotlar |
ValueTask<T> | Çoğu zaman senkron biten iş | Ölçülmüş sıcak yol; ileri seviye |
void | Çağıranın bekleyemediği iş | Yalnız event handler; başka hiçbir yerde |
IAsyncEnumerable<T> | Akış hâlinde gelen dizi | await foreach ile tüketilir |
İki pratik ayrıntı. Birincisi adlandırma: asenkron metotlar
Async son ekiyle biter (KaydetAsync) — .NET genelinde
beklenen sözleşmedir. İkincisi sıcak task (hot task): bir async metot
çağrıldığı anda çalışmaya başlar; await onu başlatmaz, bekler.
Bu ayrım paralelliğin anahtarıdır:
// Sırayla: toplam süre = a + b
var x = await AAsync();
var y = await BAsync();
// Birlikte: her ikisi de ÇAĞRILDIĞI anda başladı; toplam süre = max(a, b)
Task<int> ta = AAsync();
Task<int> tb = BAsync();
int x2 = await ta;
int y2 = await tb;
04await'ten sonra kod nerede koşar
await gördüğünde derleyicinin ürettiği kod önce şunu sorar:
beklenen iş zaten bitmiş mi? Bittiyse hiçbir şey olmaz, kod aynı thread'de
düz devam eder (bu sık görülen ve ucuz olan yoldur). Bitmemişse metodun geri kalanı
bir devam işi (continuation) olarak kaydedilir ve metot, çağıranına
tamamlanmamış bir Task döndürerek geri döner. Çağıran da await
ediyorsa aynısını yapar; zincir yukarı doğru böyle çözülür ve en tepede thread
serbest kalır.
İş bitince devam işi nerede koşar? Cevabı SynchronizationContext belirler: "devamı benim kurallarımla çalıştır" diyen bir bağlam nesnesi.
- UI uygulamalarında (WinForms, WPF, MAUI) bir bağlam vardır ve devamı
UI thread'ine geri taşır. Bu nedenle
awaitsonrası doğrudan arayüz güncelleyebilirsin — bu bir kolaylıktır, ama az sonraki deadlock'un da sebebidir. - ASP.NET Core'da bağlam yoktur (eski ASP.NET Framework'te vardı). Devam
işi havuzdaki herhangi bir thread'de koşar; istek başlarken kullandığın thread ile
awaitsonrasındaki thread farklı olabilir — kodun buna bağlı olmaması gerekir. - Konsol uygulamasında da bağlam yoktur.
ConfigureAwait(false), "devamı bağlama geri taşıma, havuzda devam
et" demektir:
string govde = await istemci.GetStringAsync(url).ConfigureAwait(false);
// buradan sonra UI thread'i GARANTİ DEĞİL — arayüze dokunma
- Kütüphane yazıyorsan kural: her
await'eConfigureAwait(false). Kütüphanen kimin bağlamında çağrılacağını bilemez; bağlama geri dönmek hem gereksiz maliyet hem de deadlock riskidir. - Uygulama kodunda (özellikle ASP.NET Core) gereksizdir; zaten bağlam
yoktur. UI kodunda ise
awaitsonrası arayüze dokunacaksan kullanma.
Son bir davranış farkı, 4. dersle bağlanır: await bir hata
yakaladığında istisnayı olduğu gibi, yığın izini koruyarak yeniden fırlatır —
bu yüzden asenkron kodu try/catch ile sarmalamak senkron koddaki
gibi çalışır. .Result ise istisnayı AggregateException
içine sarar; hata mesajlarının "bir ya da daha fazla hata oluştu" diye başlaması
bunun izidir.
05İki klasik tuzak: sync-over-async ve async void
Birincisi: asenkron bir metodu .Result, .Wait()
ya da .GetAwaiter().GetResult() ile senkron beklemek — literatürdeki
adıyla sync over async. UI uygulamasında bu kesin bir deadlock reçetesidir:
// UI thread'inde (buton olayında) çalışıyor:
string s = RaporGetirAsync(7).Result; // 1) UI thread BLOKE oldu, bekliyor
// 2) HTTP bitti; devam işi "UI thread'ine dön" diyor
// 3) UI thread meşgul — 1. adımda bloke; devam edemez
// 4) İki taraf birbirini bekliyor: KİLİT. Uygulama sonsuza kadar donar.
Bu, Hat 02'nin 5. dersindeki dairesel bekleme koşulunun aynısıdır; yalnız
kilitler yerine bağlam ve thread vardır. ASP.NET Core'da bağlam olmadığı için bu
klasik deadlock görülmez — ama tehlike bitmez: bloke edilen her istek bir
havuz thread'i tutar ve yük altında thread havuzu açlığı (thread pool
starvation) doğar. Belirtisi çok tanıdıktır: CPU düşük, istekler yavaş, uygulama
yük arttıkça daha da yavaşlar. Kural nettir: async all the way — zinciri
senkron bir halkayla kesme. Main bile artık
static async Task Main() olabilir.
İkincisi: async void. Görünüşte zararsızdır, üç şeyi birden
bozar:
- Çağıran bekleyemez: geri dönen bir Task yoktur; iş bitmeden akış devam eder.
- İstisna yakalanamaz:
async voidiçindeki bir hata çağıranıntry/catch'ine düşmez; bağlama fırlatılır ve tipik olarak süreci çökertir. - Test edilemez: bittiğini anlamanın yolu yoktur.
Tek meşru kullanım, 7. dersin senedidir: event handler'lar. Bir event
imzası void döndürdüğü için handler'ı asenkron yapmanın başka yolu
yoktur. Kural o zaman şudur — handler'ın gövdesini baştan sona
try/catch ile sarmala ve gerçek işi await edilebilir bir metoda
devret:
private async void KaydetTiklandi(object gonderen, EventArgs e) // tek istisna: event handler
{
try { await KaydetAsync(); }
catch (Exception ex) { HataGoster(ex); } // yakalanmazsa süreç çöker
}
06Birlikte bekleme: WhenAll, WhenAny ve hata toplama
Birbirinden bağımsız üç çağrıyı sırayla await etmek, sürelerini
toplamak demektir. Task.WhenAll hepsini birden bekler:
Task<Musteri> tm = MusteriGetirAsync(id);
Task<List<Siparis>> ts = SiparisleriGetirAsync(id);
Task<decimal> tb = BakiyeGetirAsync(id);
await Task.WhenAll(tm, ts, tb); // süre = en yavaşı
var ozet = new Ozet(tm.Result, ts.Result, tb.Result); // tamamlandığı KESİN: .Result burada güvenli
Hata davranışı 4. dersin senedini öder: WhenAll'un döndürdüğü Task
await edilirse yalnızca ilk istisna fırlatılır. Hepsini görmek
istiyorsan Task nesnesinin Exception özelliğine bakmalısın — orada
AggregateException vardır ve InnerExceptions koleksiyonu
tüm hataları taşır:
var hepsi = Task.WhenAll(t1, t2, t3);
try { await hepsi; }
catch (Exception)
{
foreach (var hata in hepsi.Exception!.InnerExceptions) // hepsi burada
Logla(hata);
}
Task.WhenAny ise ilk biteni verir; yedekli çağrı ("iki kaynaktan
hangisi önce cevaplarsa") ve klasik zaman aşımı deseni bununla kurulurdu. .NET 6'dan
beri zaman aşımı için daha temiz bir yol var: await isTask.WaitAsync(TimeSpan.FromSeconds(5));
Üç saha uyarısı:
- Her şeyi paralelleştirme.
DbContextgibi thread-safe olmayan nesneler üzerinde aynı anda iki işlem başlatmak çalışma zamanı hatası verir; paralellik yalnız gerçekten bağımsız işler içindir. - Ateşle-unut yasağı.
awaitedilmeyen bir Task'ın istisnası sessizce yutulur ve iş bitmeden uygulama kapanabilir. "Sonucu umursamıyorum" diyorsan bile Task'ı tut, en azından hatasını logla. - Sınırsız paralellik yük demektir. 10.000 kaydı
Select(...)+WhenAllile aynı anda göndermek karşı tarafı devirir; toplu işlerde eşzamanlılığı sınırlamak gerekir (SemaphoreSlimya daParallel.ForEachAsync'inMaxDegreeOfParallelismayarı).
07İptal, zaman aşımı ve saha kararları
.NET'te iptal işbirlikçidir (cooperative): kimse bir işi zorla durdurmaz;
bir iptal isteği yayınlanır, çalışan kod onu düzenli aralıklarla kontrol
eder ve kendisi çıkar. Taşıyıcı tip CancellationToken, kaynağı
CancellationTokenSource'tur.
using var kaynak = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // 5 sn sonra otomatik iptal
public async Task<int> IsleAsync(IEnumerable<Kayit> kayitlar, CancellationToken token)
{
int sayac = 0;
foreach (var k in kayitlar)
{
token.ThrowIfCancellationRequested(); // düzenli kontrol
await YazAsync(k, token); // token'ı AŞAĞI GEÇİR — zincir kopmasın
sayac++;
}
return sayac;
}
- Token'ı taşımak zorunludur. Bir metot token alıp aşağıdaki çağrılara geçirmiyorsa iptal fiilen çalışmaz. Kod incelemesinde ilk bakılan şeydir.
- ASP.NET Core'da bedava gelir: controller action'ına bir
CancellationTokenparametresi koyarsan çerçeveHttpContext.RequestAborted'ı bağlar; kullanıcı sekmeyi kapattığında veritabanı sorgun da iptal olur. OperationCanceledExceptionhata değildir, normal akıştır. 4. dersin diliyle: bunu "beklenen durum" sayıp ayrı yakala; hata sayacına yazma, alarm üretme.- Bağlı token (
CreateLinkedTokenSource) ile "isteğin iptali veya 5 saniyelik zaman aşımı" gibi bileşik koşullar kurulur.
Kalan saha kararları, kıdemi belli eden ayrıntılardır:
| Durum | Doğru hamle |
|---|---|
| UI'da uzun süren hesap (CPU-bound) | await Task.Run(() => Hesapla()) — UI thread'i boşalt |
| ASP.NET'te senkron metodu "asenkronlaştırmak" | Yapma. Task.Run aynı havuzdan thread çalar; kazanç yok, zarar var |
Metotta hiç await yok | async'i kaldır, doğrudan Task döndür (durum makinesi maliyeti kalksın) |
| Sıcak yolda çoğu zaman önbellekten dönen çağrı | ValueTask<T> — ama yalnız ölçtüysen; bir kez await edilir, saklanmaz |
| Milyon satırlık sonucu işlemek | IAsyncEnumerable<T> + await foreach — belleğe hepsini almadan akıt |
| Lambda'ya asenkron gövde vermek | Func<Task> al; Action alan bir API'ye async lambda verirsen async void üretmiş olursun |
08Mini alıştırma — Ölç, kilitle, iptal et
Yaklaşık 20 dakika · dotnet run
- Sıralı mı birlikte mi:
Task<int> IsAsync(int no)yaz (await Task.Delay(1000)yapıpnodöndürsün). Beş işi önce sıraylaawaitet, sonra beşini birden başlatıpTask.WhenAllile bekle. İki süreyiStopwatchile ölç ve farkı tek cümleyle açıkla. - Sıcak task kanıtı:
IsAsync'in ilk satırına ekrana yazan bir satır koy.var t = IsAsync(1);yazıp await etmeden arayaConsole.WriteLine("çağrıdan sonra")koy. Hangi satır önce yazıldı? Bu, "await başlatmaz, bekler" cümlesinin kanıtıdır. - Hata toplama: üç task kur; ikisi farklı istisna fırlatsın.
await Task.WhenAll(...)'ıtry/catchile sarmala — catch'e kaç istisna düştü? Sonra Task nesnesininException.InnerExceptionslistesini yazdır: kaç tane var? Farkı not et. - İptal: 10 saniyelik bir döngü yaz (her turda
await Task.Delay(500, token)),CancellationTokenSource'u 2 saniyeye ayarla.OperationCanceledException'ı ayrı yakala ve "iptal edildi, N tur tamamlandı" yazdır. Sonra token'ıTask.Delay'e geçirmeden tekrar dene: iptal ne zaman etkili oluyor? - async void kanıtı:
async void Patla()metodu yaz;await Task.Delay(100)sonrası istisna fırlatsın. Çağrısınıtry/catchiçine al veConsole.ReadLine()ile programı bekletip gör: catch çalışmadı, süreç çöktü. Aynısınıasync Taskimzasıyla tekrarla — bu kez yakalandı. Bu deney tek başına dersin en önemli kuralını kanıtlar.
09Kendini sına
async/await tam olarak ne yapar? "async kodu hızlandırır" doğru mu, async yeni thread açar mı?
async/await, I/O beklerken thread'i serbest bırakmayı sağlar: derleyici metodu bir durum makinesine çevirir, her await noktasını durak yapar, iş bitince metot kaldığı yerden devam eder. "Hızlandırır" yanlıştır — tek bir işlem asenkron olduğu için hızlanmaz, hatta durum makinesi ek yükü kadar yavaşlar; kazanç ölçeklenebilirliktir: aynı donanım daha çok eşzamanlı işi taşır. "Yeni thread açar" da yanlıştır: asenkron I/O beklerken hiçbir thread harcanmaz, iş bitince devam işi havuzdaki bir thread'de koşar. Yeni thread yalnız Task.Run gibi açıkça CPU-bound işi taşıdığında devreye girer — ki o da asenkronluk değil paralelliktir.
.Result / .Wait() deadlock'u nasıl oluşur? ConfigureAwait(false) ne yapar ve ASP.NET Core'da durum nedir?
SynchronizationContext'i olan bir ortamda (WinForms/WPF, eski ASP.NET) UI thread'i .Result ile bloke edilir; içerideki await'in devam işi "bağlama, yani UI thread'ine dön" der; UI thread meşgul olduğu için devam iş çalışamaz, iş bitmediği için .Result serbest kalmaz — dairesel bekleme, kilit. ConfigureAwait(false) devam işinin bağlama dönmesini engeller, havuzda devam ettirir; bu yüzden kütüphane kodunda kuraldır ve deadlock'u kırar (UI kodunda await sonrası arayüze dokunacaksan kullanılmaz). ASP.NET Core'da SynchronizationContext yoktur, dolayısıyla klasik deadlock görülmez; ama sync-over-async yine de her istekte bir havuz thread'i bloke eder ve yük altında thread havuzu açlığına yol açar — belirtisi düşük CPU'ya rağmen tırmanan yanıt süreleridir. Doğru kural her iki ortamda da aynıdır: async all the way.
async void neden kaçınılması gereken bir imzadır, tek istisnası nedir?
async void döndüren metot bir Task üretmez: çağıran işin bitmesini bekleyemez, sonucunu ya da hatasını göremez, testte tamamlanmasını bekleyemez. En tehlikelisi istisna davranışıdır — hata çağıranın try/catch'ine düşmez, doğrudan SynchronizationContext'e/thread havuzuna fırlatılır ve tipik olarak süreci çökertir. Tek meşru kullanımı event handler'lardır, çünkü event imzası void döndürür ve başka seçenek yoktur; orada da kural, handler gövdesini baştan sona try/catch ile sarmalamak ve asıl işi await edilebilir bir async Task metoduna devretmektir. Aynı tuzak, Action tipi bekleyen bir API'ye async lambda vermekte de gizlidir: derleyici orada da async void üretir.
WhenAll ile sırayla await arasındaki fark nedir? WhenAll'da hatalar nasıl toplanır, CancellationToken nasıl çalışır?
Sırayla await toplam süreyi toplar; WhenAll bağımsız işleri birlikte bekler ve süreyi en yavaş işe indirir (async metotlar çağrıldığı anda başladığı için, task'ları önce başlatıp sonra beklemek de aynı sonucu verir). WhenAll'un döndürdüğü task await edildiğinde yalnız ilk istisna fırlatılır; tüm hatalar Task.Exception içindeki AggregateException'ın InnerExceptions koleksiyonundadır. Paralellik ancak işler gerçekten bağımsızsa doğrudur — DbContext gibi thread-safe olmayan nesnelerde yasaktır — ve sınırsız paralellik karşı tarafı devirir, eşzamanlılık sınırlanır. İptal işbirlikçidir: kimse işi zorla durdurmaz; CancellationTokenSource bir istek yayınlar, kod ThrowIfCancellationRequested ile kontrol eder ve token'ı alt çağrılara geçirir — geçirilmezse iptal etkisizdir. OperationCanceledException hata değil normal akıştır; ASP.NET Core action'ında token parametresi RequestAborted'a bağlanır.
10Sınav köşesi
Yazılıda nasıl sorulur
(1) Tanım/ayrım: "senkron ile asenkron farkı", "Task ile Thread farkı",
"async yeni thread açar mı?" (hayır). (2) Doğru/yanlış: "async metot kodu
hızlandırır" (yanlış — ölçeklenebilirlik). (3) Kod okuma: sırayla üç await ile
WhenAll sürümünün toplam süresini karşılaştırma; sıcak task sorusu
("await etmeden çağırırsam iş başlar mı?" — evet). (4) Tuzak sorusu:
.Result içeren kod parçası verilip "bu kod neden kilitlenir?"
(5) İmza sorusu: async void nerede kullanılır (yalnız event
handler), istisnası nereye gider. (6) Kavram: CancellationToken
işbirlikçi iptal; OperationCanceledException hata mı (değil).
(7) Çapraz: AggregateException hangi durumda görülür — Hat 03'ün
4. dersiyle birlikte sorulur.
Sözlüde nasıl anlatılır — 2 dakikalık taslak
"async/await'i anlat" için düzen: (1) Çekirdek — "amaç hız değil,
beklerken thread tutmamak; I/O beklerken thread serbest kalır, aynı sunucu kat
kat fazla isteği taşır." (2) Mekanizma — "derleyici metodu durum makinesine
çevirir, await'ler durak olur; iş bitince devam işi çalışır — nerede
çalışacağını SynchronizationContext belirler, ASP.NET Core'da bağlam yoktur."
(3) Pratik katman — "bağımsız işleri WhenAll ile birlikte beklerim, token'ı
zincir boyunca geçiririm, kütüphane kodunda ConfigureAwait(false)
kullanırım." (4) Kıdem vuruşu — ".Result ve .Wait()
kullanmam: UI'da deadlock, ASP.NET'te thread havuzu açlığı üretir — belirtisi
düşük CPU'ya rağmen tırmanan yanıt süreleridir; async void'i
yalnız event handler'da, o da try/catch ile kullanırım." Bu son iki cümle,
async'i ezberden bilenle üretimde kullananı ayıran cevaptır.
