Mert Özen Her Satırda Daha İleri
Backend .NET Core Web API — #16

Async/await: API'de doğru asenkron kullanım

Mert Özen 15 Ağu 2026 13 dk 33 görüntülenme
Async/await: API'de doğru asenkron kullanım

Async/await'in gerçekte ne yaptığını işliyoruz: senkron ve asenkron farkı, Task nedir, bir API'nin neden asenkron olması gerektiği, ölçeklenebilirliğe etkisi ve sık yapılan hatalar.

Bu seri boyunca kod örneklerinde sürekli async, await ve Task gördün ama hep üstünkörü geçtik. "Şimdilik böyle yazıyoruz, sonra anlatacağım" dedik. İşte o an geldi. Bugün bu üç kelimenin gerçekte ne yaptığını, bir API'nin neden asenkron olması gerektiğini ve bunu yanlış kullandığında ne olduğunu konuşuyoruz. Bu konu, uygulamanın ne kadar yük kaldırabileceğini doğrudan etkiler.

Önce Senkron Dünyayı Görelim

Senkron kod, işleri sırayla yapar; bir iş bitmeden diğerine geçmez. Çoğu zaman bu gayet iyidir. Ama bazı işler doğaları gereği yavaştır: veritabanından veri çekmek, bir dosyayı okumak, başka bir servise istek atmak. Bu işler sırasında kod, sonucun gelmesini bekler. Sorun da tam burada.

Bunu bir restoranda tek bir garson gibi düşün. Garson bir masadan sipariş alıyor, sonra mutfağa gidiyor ve yemek pişene kadar orada dikilip bekliyor. Yemek hazır olana dek başka hiçbir masayla ilgilenmiyor. Diğer müşteriler boş boş oturuyor, çünkü garson tek bir siparişin başında donup kalmış. İşte senkron kod, veritabanı cevabını beklerken tam olarak bunu yapar: değerli bir kaynağı, hiçbir şey yapmadan bekleterek harcar.

Asenkron Dünyaya Geçiş

Şimdi aynı garsonu daha akıllı hale getirelim. Siparişi mutfağa veriyor ama orada dikilip beklemiyor; mutfak "hazır olunca sana haber veririm" diyor ve garson bu sırada gidip başka masalarla ilgileniyor. Yemek hazır olduğunda mutfak seslenıyor, garson gelip yemeği alıyor. Aynı garson, artık aynı anda çok daha fazla masaya hizmet edebiliyor. İşte asenkron programlamanın özü budur: bekleme anında kaynağı serbest bırakmak.

Kod tarafında bu "garson", bir thread'dir. Bir web sunucusunun elinde sınırlı sayıda thread vardır ve her gelen isteği bir thread karşılar. Senkron kodda thread, veritabanı cevabını beklerken bloke olur, başka hiçbir isteğe bakamaz. Asenkron kodda ise thread beklemeye girmez; bekleme süresince serbest kalır ve başka istekleri karşılar. Sonuç gelince kaldığı yerden devam eder.

Task, async ve await Ne İşe Yarıyor?

Bu üç kavramı tek tek yerine oturtalım. Task, "gelecekte tamamlanacak bir iş" demektir. Bir metot Task döndürüyorsa, "sana sonucu hemen değil, iş bitince vereceğim" diyor. Sonuç bir değer taşıyorsa Task<User> gibi yazılır; taşımıyorsa sadece Task.

async, bir metodun asenkron olduğunu ve içinde await kullanabileceğini işaretler. Metodun başına konur ve derleyiciye "bu metot beklemeli işler içerebilir" der. await ise asıl sihrin gerçekleştiği yerdir: bir Task'in önüne konur ve "bu iş bitene kadar bekle, ama beklerken thread'i serbest bırak" anlamına gelir. Kod okunuş olarak yukarıdan aşağı, sıradan bir kod gibi akar; ama await satırında thread arka planda serbest kalır.

public async Task GetByIdAsync(int id)
{
    var user = await _context.Users.FindAsync(id);
    return user;
}

Bu metotta olan şu: await _context.Users.FindAsync(id) satırına gelindiğinde veritabanına gidilir. Cevap beklenirken thread serbest bırakılır ve başka isteklere hizmet eder. Veritabanı cevap verince metot kaldığı yerden devam eder ve kullanıcıyı döner. Sen bu karmaşık dansı düşünmezsin bile; await yazman yeterli.

Bir API Neden Asenkron Olmalı?

İşin can alıcı noktası burası. Bir web API'si aynı anda yüzlerce, binlerce isteğe cevap vermek zorundadır ve elindeki thread sayısı sınırlıdır. Eğer her istek, veritabanı cevabını senkron bekleyerek bir thread'i bloke ederse, thread'ler hızla tükenir. Yeni gelen istekler sırada bekler, uygulama yavaşlar ve yük altında tıkanır.

Asenkron kodda ise thread'ler bekleme anında serbest kaldığı için, aynı sayıda thread çok daha fazla isteğe hizmet edebilir. Uygulaman aynı donanımla kat kat fazla yük kaldırır. Yani async/await sadece "güzel bir alışkanlık" değil; bir API'nin ölçeklenebilirliğinin temelidir. Bu yüzden EF Core, veritabanı işlemleri için ToListAsync, FindAsync, SaveChangesAsync gibi asenkron metotlar sunar ve gerçek projelerde bunları kullanman beklenir.

Sık Yapılan Hatalar

Async/await güçlüdür ama birkaç klasik tuzağı vardır. En tehlikelisi, asenkron bir metodu .Result veya .Wait() ile senkron biçimde çağırmaktır. Bu, tüm asenkron faydayı yok eder ve bazı durumlarda uygulamanın tamamen kilitlenmesine (deadlock) yol açabilir. Kural nettir: async bir metodu çağırırken await kullan, .Result'a asla dokunma.

İkinci sık hata, asenkron olabilecek bir işi senkron yapmaktır. Elinde ToListAsync varken ToList kullanmak, garsonu yine mutfakta bekletmek demektir. Veritabanı, dosya ya da ağ gibi bekleme içeren her işte asenkron versiyonu tercih et.

Üçüncü olarak, "async her yeri sarar" prensibini unutma. Bir metot asenkron bir iş çağırıyorsa, kendisi de asenkron olmalı ve onu çağıran da await etmeli. Bu zincir controller'a kadar uzanır; işte bu yüzden action metotlarımızı async Task<IActionResult> olarak yazıyoruz. Zinciri ortada senkrona çevirmek, tüm faydayı kırar.

Küçük Bir Deneme

Elindeki bir servis metodunu al ve iki versiyonunu yaz: biri EF Core'un senkron metotlarıyla (ToList, Find), diğeri asenkron metotlarıyla (ToListAsync, FindAsync ve await). Controller tarafını da buna göre bir senkron, bir async Task olacak şekilde ayarla. İkisi de aynı sonucu dönecek; ama artık aradaki kavramsal farkı, yani birinin thread'i bekletip diğerinin serbest bıraktığını biliyorsun. Kodun ikisinde de çalıştığını görmek, async yazmanın aslında ne kadar doğal olduğunu gösterir. Gerçek performans farkını görmek yük testi gerektirir ama mantığı kavramak için bu karşılaştırma yeterli.

Bir sonraki yazıda pagination, filtering ve sorting konusuna geçeceğiz: bir liste endpoint'i binlerce kayıt döndüğünde bunları nasıl sayfalara böleceğimizi, nasıl filtreleyip sıralayacağımızı konuşacağız. Bugün kavradığın asenkron mantık, o büyük veri sorgularını verimli çalıştırmanın da temeli olacak.