Asenkron Programlamada Performans: I/O-bound vs. CPU-bound
Async/await, .NET'te eşzamanlılık denince akla gelen ilk araçtır. Ancak yanlış kullanıldığında, performans kazancı sağlamak bir yana, ek yük ve gizli hatalar üretebilir. Bu yazının merkezinde tek bir soru var: İşlem gerçekten asenkron mu, yoksa CPU'yu meşgul eden bir iş mi? Cevap, doğru deseni belirler.
1. I/O-bound vs. CPU-bound: Temel Ayrım
| Özellik | I/O-bound | CPU-bound |
|---|---|---|
| Ne yapar? | Ağ, disk, veritabanı, HTTP çağrısı bekler | Hesaplama yapar (matematik, şifreleme, görüntü işleme) |
| Darboğaz | Gecikme (latency), bekleme süresi | CPU çekirdekleri, işlem gücü |
| Async/await faydası | ✅ Yüksek (thread serbest kalır) | ❌ Yok (thread zaten meşgul) |
| Doğru araç | async/await |
Task.Run, Parallel.For, PLINQ |
| Örnek | await httpClient.GetAsync(url) |
await Task.Run(() => Hesapla()) |
Kritik Nokta: Async/await, bir işlemi daha hızlı yapmaz; yalnızca beklerken thread'i serbest bırakır. CPU-bound bir işte thread zaten çalışıyor olduğu için, async eklemek yalnızca state machine maliyeti ekler.
2. I/O-bound İşlemlerde Async/await: Gerçek Kazanç
Bir HTTP çağrısı veya veritabanı sorgusu düşünün. Senkron kodda, thread bu işlem tamamlanana kadar bloke olur; bu süre boyunca başka istekleri işleyemez. Async/await ile thread, I/O işlemi sürerken serbest kalır ve başka istekleri karşılayabilir.
Senkron (Bloke):
csharp
public Product GetProduct(int id)
{
// Thread burada bekler; başka istek işleyemez
var response = _httpClient.GetStringAsync($"api/products/{id}").Result;
return JsonSerializer.Deserialize<Product>(response);
}
Asenkron (Serbest):
csharp
public async Task<Product> GetProductAsync(int id)
{
// Thread I/O sürerken serbest kalır
var response = await _httpClient.GetStringAsync($"api/products/{id}");
return JsonSerializer.Deserialize<Product>(response);
}
Ölçeklenebilirlik Etkisi: 1.000 eşzamanlı istek geldiğinde, senkron yaklaşım 1.000 thread gerektirir (thread pool tükenir); asenkron yaklaşım ise I/O tamamlanana kadar thread'leri serbest bırakır, çok daha az thread ile aynı yük karşılanır.
3. CPU-bound İşlemlerde Async: Yanılgı ve Doğru Kullanım
CPU-bound bir işlemi async metot içinde yapmak, onu asenkron yapmaz. Yalnızca gereksiz bir state machine ve ek yük ekler.
Yanlış:
csharp
public async Task<int> HesaplaAsync(int x, int y)
{
await Task.Delay(1); // Anlamsız; sadece async görünümü veriyor
return x + y; // CPU işi hala aynı thread'de
}
Doğru (CPU işini ayrı thread'e taşı):
csharp
public async Task<int> HesaplaAsync(int x, int y)
{
return await Task.Run(() => AgirHesaplama(x, y));
}
Ancak Dikkat: Task.Run her CPU işi için doğru cevap değildir. ASP.NET Core gibi bir sunucuda, CPU-bound işi Task.Run ile thread pool'a atmak, thread pool'u tüketebilir ve ölçeklenebilirliği azaltabilir. Bu durumda doğru yaklaşım, işi ayrı bir arka plan servisine (BackgroundService, kuyruk) taşımaktır.
4. ConfigureAwait(false): Kütüphane Kodu İçin Kritik
await sonrası kodun, orijinal context'e (UI thread, ASP.NET request context) dönmesi gerekmediğinde ConfigureAwait(false) kullanmak, gereksiz context switch'ini önler.
csharp
// Kütüphane kodu: context'e dönmeye gerek yok
public async Task<string> FetchDataAsync()
{
var response = await _httpClient.GetAsync(url).ConfigureAwait(false);
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
Ne Zaman Kullanılmalı? Uygulama kodu değil, kütüphane kodu için. UI uygulamalarında, UI thread'ine dönmek gerekebilir; bu durumda ConfigureAwait(false) kullanılmamalıdır.
5. Sık Yapılan Hatalar
| Hata | Sonuç | Çözüm |
|---|---|---|
.Result veya .Wait() kullanmak |
Deadlock, thread bloke | await kullanın |
async void kullanmak |
Hata yakalanamaz, test edilemez | async Task kullanın |
| CPU-bound işi async sanmak | Ek yük, kazanç yok | Task.Run veya arka plan servisi |
| I/O'yu senkron bloke etmek | Thread pool tükenmesi | Async API kullanın |
Gereksiz Task.Run (ASP.NET'te) |
Thread pool baskısı | İşi arka plana taşıyın |
6. En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| İşlem türünü belirleyin | I/O mu, CPU mu? Doğru desen buna bağlıdır. |
| I/O'da async/await kullanın | Veritabanı, HTTP, dosya işlemlerinde thread'i serbest bırakın. |
| CPU'da paralel işleme kullanın | Parallel.For, PLINQ veya arka plan servisi. |
| Senkron API'leri bloke etmeyin | .Result ve .Wait() deadlock riski taşır. |
Kütüphane kodunda ConfigureAwait(false) |
Gereksiz context switch'ini önleyin. |
Sonuç
Async/await, I/O-bound işlemlerde ölçeklenebilirliğin temel taşıdır; thread'i bekleme süresince serbest bırakarak çok daha az kaynakla çok daha fazla isteği karşılamanızı sağlar. Ancak CPU-bound işlemlerde async eklemek, kazanç sağlamaz; yalnızca ek yük getirir. Doğru yaklaşım, işlemin türünü doğru teşhis etmek ve uygun aracı seçmektir: I/O için async/await, CPU için paralel işleme veya arka plan servisi. Unutmayın: Async/await bir performans sihirbazı değil, bir ölçeklenebilirlik aracıdır; onu doğru yerde kullanmak, yanlış yerde kullanmaktan çok daha değerlidir.