Merhaba,
Async ve await, modern yazılım geliştirmede sık kullanılan ve özellikle performans açısından kritik öneme sahip anahtar kelimelerdir. Ancak çoğu zaman bu kavramlar, sadece nasıl kullanılacağı bilinen ama arkasındaki çalışma mantığı tam olarak anlaşılmayan yapılar olarak kalır.
Bu durum küçük ölçekli uygulamalarda fark edilmese de, gerçek dünyadaki sistemlerde ciddi problemlere yol açabilir. Özellikle aynı anda birçok isteğin işlendiği uygulamalarda, bu yapıların yanlış veya yüzeysel kullanımı performans kayıplarına ve gereksiz kaynak tüketimine neden olur.
async ve await yapısını doğru anlamak, sadece kodun çalışmasını sağlamakla kalmaz; aynı zamanda uygulamanın daha verimli, daha ölçeklenebilir ve daha sürdürülebilir olmasına katkı sağlar. Bu da geliştiricinin, sistemin nasıl çalıştığını daha bilinçli şekilde yönetebilmesini sağlar.
Bu yazıda konuyu mümkün olduğunca sade bir şekilde ele alacak, gereksiz teorik detaylara boğmadan ama mantığını netleştirerek ilerleyeceğim. Amacım sadece kullanımını göstermek değil, neden bu şekilde çalıştığını da anlaşılır hale getirmek.
Restoran Örneği
Bir restoranınız olduğunu düşünün ve mutfakta sadece tek bir aşçı çalışıyor.
Senkron Çalışma:
Bir müşteri sipariş verir. Aşçı patatesleri kızartmaya başlar ve pişmesini bekler. Bu süreç yaklaşık 5 dakika sürer. Ancak burada kritik nokta şu: aşçı bu süre boyunca başka hiçbir işle ilgilenmez. Yeni sipariş almaz, hazırlık yapmaz, sadece patatesin kızarmasını bekler. Bu sırada dışarıda bekleyen müşteri sayısı artar. Sistem çalışıyordur, ama verimli değildir. Çünkü aşçının zamanı, aktif iş yapmak yerine beklemekle geçmektedir.
Asenkron Çalışma:
Aşçı yine patatesleri kızartmaya başlar, ancak bu sefer sürecin başında beklemez. Patatesleri yağın içine bırakır, zamanlayıcıyı kurar ve bu süre zarfında başka işlere yönelir. Yeni gelen müşterinin siparişini alır, başka bir yemeğin hazırlığını yapar ya da ön hazırlık işlerini tamamlar. Patatesler hazır olduğunda (örneğin bir zamanlayıcı ile), aşçı geri dönüp işlemi tamamlar.
Sonuç:
Aşçı sayısı değişmez, yani sistemin kaynağı aynıdır. Ancak aşçının zamanı artık daha verimli kullanıldığı için aynı sürede daha fazla iş yapılabilir. Buradaki önemli nokta şu: sistem daha hızlı çalışmaz, ama boşta bekleyen süreler ortadan kaldırıldığı için toplam verimlilik ciddi şekilde artar. Yazılımdaki asenkron yapı da tam olarak bunu yapar; işlem süresini kısaltmaz, bekleme süresini daha iyi değerlendirir.
Gerçek Dünya Senaryosu
Bir sipariş detay sayfası geliştirdiğinizi düşünün. Bu sayfa açıldığında arka planda birden fazla dış kaynaktan veri toplanması gerekiyor:
- Veritabanından kullanıcı bilgileri (200 ms)
- Kargo servisinden takip durumu (500 ms)
- Ödeme sisteminden fatura bilgisi (300 ms)
Bu üç işlem birbirinden bağımsızdır. Yani teknik olarak aynı anda yapılmalarının önünde bir engel yoktur. Ancak nasıl yazdığınız, sistemin davranışını tamamen değiştirir.
Senkron
Bu yöntemde işlemler tek tek ve sırayla çalıştırılır. Bir işlem bitmeden diğeri başlamaz.
public IActionResult SiparişDetayı(int siparisId)
{
// Her satır bir sonrakini kilitler (Blocking)
var kullanici = _userService.GetById(siparisId);
var kargoDurumu = _kargoService.GetKargoDurumu(siparisId);
var fatura = _odemeService.GetFatura(siparisId);
return View(new SiparisViewModel(kullanici, kargoDurumu, fatura));
}
Burada önemli nokta sadece sırayla çalışması değil. Asıl problem şu: Her bir satır çalıştığında, ilgili işlem tamamlanana kadar o isteği işleyen thread hiçbir şey yapmadan bekler. Yani:
-
200 ms boyunca thread boşta bekler
-
sonra 500 ms daha bekler
-
sonra 300 ms daha bekler
Toplamda yaklaşık 1000 ms (1 saniye) boyunca thread aktif olarak iş üretmez, sadece dış sistemlerden cevap gelmesini bekler. Bu durum tek bir kullanıcı için küçük görünebilir. Ancak aynı anda yüzlerce istek geldiğinde thread pool hızla dolar, yeni gelen istekler kuyruğa girer uygulama yavaşlamaya başlar sadece kullanıcı biraz bekliyor değil; sistemin ölçeklenebilirliği doğrudan zarar görür.
Asenkron
Bu örnekte async ve await kullanılıyor. Bu sayede thread, bekleme sırasında bloklanmaz ve başka isteklere hizmet verebilir.
public async Task<IActionResult> SiparişDetayı(int siparisId)
{
// await gördüğü an thread boşa çıkar ve diğer isteklere koşar
var kullanici = await _userService.GetByIdAsync(siparisId);
var kargoDurumu = await _kargoService.GetKargoDurumuAsync(siparisId);
var fatura = await _odemeService.GetFaturaAsync(siparisId);
return View(new SiparisViewModel(kullanici, kargoDurumu, fatura));
}
Bu yaklaşımın sağladığı avantaj şudur: Thread, her await noktasında boşa çıkar ve bu sırada başka istekleri işleyebilir. Yani sistem genelinde daha iyi bir kaynak kullanımı sağlanır ve uygulama daha ölçeklenebilir hale gelir. Ancak burada gözden kaçan önemli bir detay var:
Her await ifadesi, bir önceki işlemin tamamlanmasını bekler. Yani işlemler hâlâ sıralı şekilde çalışır:
-
Önce kullanıcı bilgisi alınır (200 ms)
-
Sonra kargo durumu sorgulanır (500 ms)
-
Sonra fatura bilgisi çekilir (300 ms)
Toplam süre yine yaklaşık 1000 ms olur.
Yani: Thread bloklanmaz ama işler paralel çalışmaz Bu nedenle bu kod, senkron versiyona göre ölçeklenebilirlik açısından daha iyi, ama response süresi açısından aynı kalır.
Paralel Asenkron
Eğer yapılan işlemler birbirinden bağımsızsa, bunları sırayla çalıştırmak yerine aynı anda başlatmak çok daha verimli bir yaklaşımdır. Bu senaryoda: Kullanıcı bilgisi, Kargo durumu, Fatura bilgisi birbirine bağlı değildir. Bu yüzden aynı anda başlatılabilirler.
public async Task<IActionResult> SiparişDetayı(int siparisId)
{
var kullaniciTask = _userService.GetByIdAsync(siparisId);
var kargoTask = _kargoService.GetKargoDurumuAsync(siparisId);
var faturaTask = _odemeService.GetFaturaAsync(siparisId);
await Task.WhenAll(kullaniciTask, kargoTask, faturaTask);
var kullanici = await kullaniciTask;
var kargoDurumu = await kargoTask;
var fatura = await faturaTask;
return View(new SiparisViewModel(kullanici, kargoDurumu, fatura));
}
Bu yaklaşımda önemli olan iki şey var: İlk olarak, tüm işlemler aynı anda başlatılır. Yani artık süreler toplanmaz. En uzun süren işlem hangisiyse toplam süre ona yakın olur. Bu örnekte yaklaşık 500 ms. İkinci olarak, await Task.WhenAll(...) satırı, tüm görevlerin bittiğinden emin olmamızı sağlar. Hemen ardından gelen await ifadeleri ise sadece tamamlanmış görevlerin içindeki veriyi çekmek için kullanılır; yani burada ikinci bir bekleme gerçekleşmez. Tekrar hatırlatayım. Bu yaklaşım her yerde kullanılmaz. Eğer işlemler arasında bağımlılık varsa (örneğin kargo sorgusu için önce kullanıcı bilgisi gerekiyorsa), paralel başlatmak hatalı sonuçlara yol açar.
Ayrıca dış sistemlere aynı anda çok fazla istek atmak: rate limit'e neden olabilir. Yani paralellik kontrolsüz artırılmamalıdır.
Dikkat Edilmesi Gerekenler
1. Zincir Kırılmamalı
Bir metodu async yaptıysanız, onu çağıran metodun da asenkron olması gerekir ve await ile çağrılmalıdır. Araya bir yerde .Result veya .Wait() koyarsanız, thread bloklanır ve uygulama deadlock durumuna girebilir.
var result = _userService.GetByIdAsync(5).Result; // Tehlikeli!
Burada thread, async işlemin tamamlanmasını beklerken kendisi de beklemeye girdiği için kilitlenir. Bunun yerine her zaman zincir boyunca await kullanın:
var result = await _userService.GetByIdAsync(5); // Güvenli
2. Void Yerine Task Kullanılmalı
Bir metodu async void yaptığınızda, programa şunu söylersiniz: “Bu işi başlat ve unut, sonucuyla veya hatasıyla ilgilenmiyorum.” Bu web/backend uygulamalarda tehlikeli çünkü:
-
Hataları yakalayamazsınız: İçindeki exception’lar kontrol edilemez, uygulama beklenmedik şekilde çökebilir.
-
Bittiğini takip edemezsiniz: İşlem ne zaman tamamlandı bilinmez, zincirleme asenkron kullanımda sorun çıkar.
Bu nedenle async void sadece UI event handler’lar için uygundur; diğer yerlerde async Task veya async Task<T> kullanmalısınız.
Doğru: async Task<int> GetDataAsync() -- Yanlış: async void GetDataAsync()
3.Async Hızlandırmaz, Verimlilik Sağlar
Asenkron kod işlem süresini kısaltmaz. Örneğin bir veritabanı sorgusu 10 ms sürüyorsa, async yaparak 5 ms’ye düşmez. Bunun yerine, o süre boyunca thread boşta beklemez; başka isteklere hizmet verebilir. Bu, uygulamanızın aynı anda daha fazla kullanıcıya yanıt vermesini sağlar. Yani asenkron programlamanın gerçek kazanımı performans değil, ölçeklenebilirlik ve kaynak verimliliğidir.
4. İsimlendirme
Asenkron metotları tanımak için metodun adına Async eklemek bir standarttır. Bu hem kodu okuyanlar için anlaşılırlık sağlar hem de yanlış kullanımı önler. Örneğin:
Doğru: GetProductsAsync()
Yanlış: GetProducts() çünkü asenkron olup olmadığını anlamak zorlaşır
5. İptal Etme
Uzun süren işlemlerde CancellationToken kullanmak gerekebilir. Örneğin, kullanıcı bir sayfayı kapattığında sunucuda gereksiz yere çalışmaya devam eden veritabanı sorguları, kaynak israfına yol açar.
public async Task<List<Product>> GetProductsAsync(CancellationToken cancellationToken)
{
return await _dbContext.Products.ToListAsync(cancellationToken);
}
Bu sayede kaynaklar verimli kullanılır ve uygulama daha dayanıklı olur.
6. Gereksiz Async’den Kaçınma
Eğer metodunuz sadece CPU üzerinde kısa hesaplamalar yapıyorsa örneğin iki sayıyı toplamak, async yapmak gereksizdir. Async yapının asıl faydası I/O-bound işlemlerde, yani disk, network veya veritabanı erişimlerinde ortaya çıkar. Gereksiz async kullanımı kod karmaşasına ve performans kaybına yol açabilir.
Sonuç
Async ve await kullanmak, başlangıçta kafa karıştırıcı gibi görünse de, bir kez mantığını kavradığınızda uygulamanızı bambaşka bir seviyeye taşıyor. Aslında buradaki iş, sadece kodun çalışmasını sağlamak değil; sistem kaynaklarını verimli kullanmak, aynı anda daha fazla kullanıcıya hizmet verebilmek ve bekleyen thread’leri boşa harcamamak. Küçük projelerde belki fark etmez, ama gerçek dünyadaki yüksek trafikli uygulamalarda bu fark, uygulamanın performansını ve ölçeklenebilirliğini belirliyor.
Diğer Bloglarımda Görüşmek Üzere 👋