
Asenkron programlama denilince birçok geliştiricinin aklına "kodun daha hızlı çalışması" geliyor. Bu, sektördeki en büyük yanılgılardan biridir. Asenkron programlama kodunuzu hızlandırmaz; aksine, çok küçük bir miktar overhead (ek yük) ekler.
Peki o zaman neden kullanıyoruz?
Mesele ölçeklenebilirlik (scalability). Bir I/O işlemi (veritabanı sorgusu, dosya okuma, API çağrısı) başladığında, işlemci aslında hiçbir şey yapmaz. Senkron bir dünyada, o thread (iş parçacığı) sonuç gelene kadar bloklama yapar.
Senkron: Garsonun siparişi mutfağa verip, yemek pişene kadar tezgahın önünde dikilip beklemesi gibidir. Yeni müşteri gelse bile ilgilenemez.
Asenkron: Garsonun siparişi verip, yemek pişerken diğer masalarla ilgilenmesi, yemek hazır olduğunda ise geri dönüp onu servis etmesidir.
1: Task Sınıfı
C# dünyasında Task, bir işlemin o anki durumunu, sonucunu ve tamamlanma bilgisini temsil eden bir nesnedir. Senior seviyesinde bir geliştirici için Task, sadece "arka planda çalışan bir iş" değil, JavaScript'teki Promise yapısına benzer bir "gelecekte tamamlanacak bir işin kontratı"dır.
Task Bir Thread Değildir!
Thread: İşletim sistemi seviyesinde bir işçidir.
Task: Yapılması gereken bir "iştir".
2. Task Nesnesinin Genel Yapısı
Bir Task nesnesi oluşturulduğunda, bir yaşam döngüsünden geçer. Bu döngüyü anlamak, hata ayıklama (debugging) yaparken hayat kurtarır:
Created / WaitingToRun: Görev oluşturuldu ama henüz planlanmadı.
Running: Görev şu an bir thread üzerinde icra ediliyor.
RanToCompletion: Görev başarıyla bitti ve varsa sonucunu paketledi.
Faulted: Görev içinde bir hata (exception) oluştu.
Canceled: Görev, bir
CancellationTokenile durduruldu.
Tasknesnesi, bu durum geçişlerini kendi içinde yönetir veawaitanahtar kelimesi aslında bu durumun "Tamamlandı" (Completed) olup olmadığını sorgular.
C# derleyicisi, async ve await anahtar kelimelerini gördüğünde arka planda karmaşık bir State Machine (Durum Makinesi) yapısı kurar. Kodunuzu parçalara böler ve "veritabanından cevap geldiğinde kaldığın yerden devam et" talimatını sisteme bırakır.
public async Task<List<User>> GetActiveUsersAsync()
{
// await kullanıldığında, bu thread veritabanını beklerken serbest kalır
// ve başka istekleri işlemeye gider.
using var connection = new SqlConnection(_connectionString);
var users = await connection.QueryAsync<User>("SELECT * FROM Users WHERE IsActive = 1");
// Veritabanı yanıt verdiğinde, müsait olan bir thread işleme buradan devam eder.
return users.ToList();
}3. Task vs ValueTask: Bellek Optimizasyonu
Senior seviyesinde kod yazarken her objenin bir maliyeti olduğunu biliriz. Task bir class'tır, yani heap üzerinde yer kaplar.
Eğer bir metodunuzun sonucu çoğu zaman hazırsa (örneğin bir cache'den dönüyorsa), her seferinde bir Task objesi "allocate" etmek gereksiz GC (Garbage Collector) baskısı yaratır. İşte burada ValueTask (bir struct) devreye girer.
Kural: Eğer metodunuzun asenkron çalışma ihtimali düşükse veya çok sık çağrılan (high-throughput) bir yerdeyse
ValueTaskkullanın. Ancak genel senaryolardaTaskhala standarttır.
4. Task.FromResult ve Task.CompletedTask
Bazen asenkron bir imza (interface) zorunluluğu nedeniyle senkron bir değeri dönmeniz gerekir. Bu durumda yeni bir asenkron süreç başlatmak yerine, önceden tamamlanmış bir task dönmek en profesyonel yaklaşımdır:
public Task<int> GetCachedValueAsync(int key)
{
if (_cache.ContainsKey(key))
return Task.FromResult(_cache[key]); // Yeni bir asenkron operasyon başlatmaz, sonucu kutular.
return FetchFromDbAsync(key);
}5. Task.WhenAll vs Task.WhenAny
Birden fazla vaadi yönetirken stratejiniz ne?
Task.WhenAll: "Hepiniz bitince haber verin." (Paralel I/O için en iyisi)Task.WhenAny: "Herhangi biriniz bitse yeter, ben devam ederim." (Örn: Birkaç farklı API'den veri bekleyip en hızlı geleni almak)
