C# Asenkron Programlama: async ve await ile Modern Uygulamaların Kilidini Açmak

Giriş: Neden Asenkron Programlama?

Günümüzün yazılım dünyasında, uygulamaların hızlı, akıcı ve yanıt veren olması beklenir. Kullanıcılar, bir düğmeye tıkladıklarında uygulamanın donmasını veya bir web sayfasının yüklenmesi için dakikalarca beklemeyi kabul etmezler. Benzer şekilde, sunucu tarafı uygulamaların aynı anda binlerce isteği verimli bir şekilde işlemesi gerekir. İşte bu noktada asenkron programlama devreye girer.

Geleneksel senkron programlama modelinde, bir görev başlatıldığında, programın geri kalanı o görevin tamamlanmasını bekler. Eğer bu görev uzun sürüyorsa (örneğin, bir ağ isteği yapmak, büyük bir dosyayı okumak veya karmaşık bir hesaplama yapmak), tüm uygulama “bloke” olur. Bu durum, kullanıcı arayüzlerinin donmasına (“Not Responding” durumu) veya sunucuların yeni istekleri kabul edememesine yol açar, çünkü çalışan iş parçacığı (thread) meşgul olur ve başka iş yapamaz.

Asenkron programlama, bu bloklama sorununu çözmek için tasarlanmıştır. Uzun süren bir işlem başlatıldığında, asenkron model iş parçacığının bloke olmasına izin vermez. Bunun yerine, iş parçacığı başka görevleri yerine getirmek üzere serbest bırakılır. Uzun süren işlem tamamlandığında, uygulama bu durumdan haberdar edilir ve işlemin sonucunu alıp kaldığı yerden devam edebilir. Bu, özellikle G/Ç (Giriş/Çıkış — I/O) işlemleri (ağ, disk, veritabanı erişimi gibi) için önemlidir, çünkü bu işlemler sırasında CPU genellikle boştadır ve başka işler yapabilir.

C#, asenkron programlamayı basitleştirmek ve daha okunabilir hale getirmek için .NET Framework 4.5 ve C# 5.0 ile birlikte async ve await anahtar kelimelerini tanıttı. Bu anahtar kelimeler, G/Ç-bağımlı (I/O-bound) ve CPU-bağımlı (CPU-bound) işlemleri verimli bir şekilde yönetmek için güçlü bir model sunar ve karmaşık geri çağırma (callback) mekanizmalarına veya manuel iş parçacığı yönetimine olan ihtiyacı büyük ölçüde azaltır.

Bu makalede, C#’taki async/await modelinin temellerini, nasıl çalıştığını, Task ve Task nesnelerinin rolünü, yaygın asenkron desenleri, hata yönetimini, iptal mekanizmalarını, en iyi pratikleri ve potansiyel tuzakları derinlemesine inceleyeceğiz. Asenkron programlamanın gücünü anladığınızda, daha performanslı, ölçeklenebilir ve kullanıcı dostu uygulamalar geliştirme yeteneğiniz önemli ölçüde artacaktır.

Bölüm 1: Senkron Dünyanın Sınırları ve Asenkron İhtiyacı

async/await’in değerini tam olarak anlamak için önce senkron programlamanın neden olduğu sorunları gözden geçirelim.

1.1. UI Uygulamalarında Bloklama Sorunu

Masaüstü (WinForms, WPF, MAUI) veya mobil uygulamalarda, kullanıcı arayüzü (UI) genellikle tek bir ana iş parçacığı (UI thread) tarafından yönetilir. Tüm UI güncellemeleri ve olay işlemeleri (buton tıklamaları, fare hareketleri) bu thread üzerinde gerçekleşir. Eğer bu UI thread’i üzerinde uzun süren senkron bir işlem (örneğin, bir web servisinden veri çekme veya büyük bir dosyayı işleme) başlatırsanız, UI thread’i bu işlem bitene kadar bloke olur.

Sonuç? Uygulama donar. Kullanıcı düğmelere tıklayamaz, pencereyi taşıyamaz, hatta bazen işletim sistemi uygulamayı “Yanıt Vermiyor” olarak işaretler ve kapatmayı önerir. Bu, son derece kötü bir kullanıcı deneyimidir.

// --- KÖTÜ ÖRNEK: Senkron Kod ile UI Bloklama ---
private void DownloadButton_Click(object sender, RoutedEventArgs e)
{
// Bu metot UI thread'inde çalışır.
statusLabel.Text = "İndirme başlıyor...";
try
{
// WebClient.DownloadString senkron bir metottur ve ağ işlemi bitene kadar
// UI thread'ini BLOKE EDER!
using (var client = new System.Net.WebClient())
{
string data = client.DownloadString("https://jsonplaceholder.typicode.com/posts/1");
// Bu satıra gelene kadar UI donacaktır.
resultTextBox.Text = data;
statusLabel.Text = "İndirme tamamlandı.";
}
}
catch (Exception ex)
{
statusLabel.Text = "Hata oluştu!";
MessageBox.Show($"Bir hata oluştu: {ex.Message}");
}
// Metot bitene kadar UI bloke kalır.
}
1.2. Sunucu Uygulamalarında Ölçeklenebilirlik Sorunu

ASP.NET (Core) gibi sunucu tarafı uygulamalarda, her gelen istek genellikle bir iş parçacığı havuzundan (thread pool) alınan bir iş parçacığı tarafından işlenir. Eğer istek işlenirken senkron bir G/Ç işlemi (örneğin, veritabanı sorgusu, harici API çağrısı) yapılırsa, bu iş parçacığı G/Ç işlemi tamamlanana kadar bloke olur.

Bu sırada iş parçacığı CPU’yu aktif olarak kullanmasa da, başka bir isteği işlemek için kullanılamaz. Eğer aynı anda çok sayıda istek gelirse ve hepsi de senkron G/Ç işlemleri nedeniyle iş parçacıklarını bloke ederse, thread havuzundaki tüm iş parçacıkları hızla tükenebilir. Bu durumda, yeni gelen istekler işlenemez veya işlenmek için uzun süre beklemek zorunda kalır. Bu, uygulamanın ölçeklenebilirliğini ciddi şekilde sınırlar ve performansını düşürür. Sunucu kaynakları (CPU, bellek) verimli kullanılmamış olur.

1.3. Asenkron Programlama Çözümü

Asenkron programlama, bu bloklama sorunlarını ele alır:

UI Uygulamalarında: Uzun süren G/Ç işlemleri arka planda başlatılır. UI thread’i bloke olmaz, uygulama yanıt vermeye devam eder. İşlem bittiğinde sonuç UI thread’ine geri gönderilir ve arayüz güncellenir.
Sunucu Uygulamalarında: Bir G/Ç işlemi başlatıldığında, istek işleyen iş parçacığı bloke olmak yerine thread havuzuna geri döner ve başka bir isteği işlemek için kullanılabilir hale gelir. G/Ç işlemi tamamlandığında, thread havuzundan uygun bir iş parçacığı (aynısı veya farklı biri olabilir) işlemin geri kalanını (sonucu işleme, yanıt gönderme) yürütmek üzere görevlendirilir. Bu, aynı anda çok daha fazla isteğin verimli bir şekilde işlenmesini sağlar ve sunucu kaynaklarının daha iyi kullanılmasına olanak tanır.
Bölüm 2: async ve await’in Temelleri

C# 5.0 ile gelen async ve await anahtar kelimeleri, asenkron kodu yazmayı ve okumayı senkron koda çok benzer bir hale getirerek büyük bir devrim yarattı.

2.1. Task ve Task: Asenkron İşlemin Temsili

async/await modelinin merkezinde System.Threading.Tasks.Task ve System.Threading.Tasks.Task türleri bulunur. Bu türler, devam eden bir asenkron işlemi temsil ederler.

Task: Bir değer döndürmeyen (yani void bir senkron metoda karşılık gelen) asenkron bir işlemi temsil eder. İşlemin tamamlanıp tamamlanmadığını, başarılı olup olmadığını veya bir hata oluşup oluşmadığını takip etmek için kullanılır.
Task: Task’tan türer ve TResult türünde bir değer döndüren asenkron bir işlemi temsil eder. İşlem tamamlandığında sonucuna (Result özelliği aracılığıyla — ancak doğrudan erişmekten kaçının!) erişilebilir.
Bir asenkron metot çağırdığınızda, genellikle hemen bir Task veya Task nesnesi geri döner. Bu nesne, asıl işlemin tamamlanacağına dair bir “söz” veya “gelecek” (promise/future) gibidir. İşlem henüz tamamlanmamış olabilir.

2.2. async Anahtar Kelimesi: Metodu Asenkron Olarak İşaretleme

Bir metodu async olarak işaretlemek, derleyiciye iki ana şey söyler:

Bu metot içinde await anahtar kelimesi kullanılabilir.
Metodun geri dönüş değeri özel olarak ele alınmalıdır. Eğer metot Task, Task veya void (genellikle kaçınılmalıdır) döndürüyorsa, derleyici bu dönüş tiplerini yönetecek kodu otomatik olarak üretir.
// Bir değer döndürmeyen asenkron metot
public async Task VeriKaydetAsync()
{
// ... asenkron işlemler ...
await Task.Delay(100); // Örnek bir asenkron bekleme
}
// Bir string değeri döndüren asenkron metot
public async Task VeriIndirAsync(string url)
{
// ... asenkron işlemler ...
await Task.Delay(100); // Örnek
string sonuc = "İndirilen Veri"; // Gerçek indirme kodu yerine
return sonuc; // Derleyici bunu Task olarak sarmalar
}
Önemli: async anahtar kelimesi, metodu otomatik olarak farklı bir iş parçacığında çalıştırmaz! Sadece await’in kullanılmasını sağlar ve metodun nasıl bölüneceğini belirler.

2.3. await Anahtar Kelimesi: Asenkron Beklemenin Sihri

await operatörü, bir Task veya Task (genellikle “awaitable” olarak adlandırılır) üzerinde kullanılır. await’in yaptığı şey şudur:

Kontrol Etme: await edilen Task’ın zaten tamamlanıp tamamlanmadığını kontrol eder.
Tamamlandıysa: Eğer Task zaten tamamlanmışsa, metot senkron olarak devam eder. Eğer Task ise, await ifadesi TResult türündeki sonucu döndürür. Eğer Task bir hata ile tamamlandıysa, hata yeniden fırlatılır.
Tamamlanmadıysa (Asıl Sihir):
await, metodun geri kalanını bir “devamlılık” (continuation) olarak kaydeder. Bu devamlılık, Task tamamlandığında çalıştırılacak olan kod parçasıdır.
Metodun kontrolünü hemen çağıran metoda geri verir. Bu, çağıran metodun (ve dolayısıyla iş parçacığının) bloke olmamasını sağlar! UI thread’i arayüzü güncellemeye devam edebilir, sunucu thread’i başka istekleri alabilir.
Arka planda, Task tamamlandığında (başarıyla veya hatayla), kaydedilen devamlılık uygun bir zamanda ve bağlamda (context) çalıştırılmak üzere zamanlanır.
Devamlılık çalıştırıldığında, metot await noktasından itibaren kaldığı yerden devam eder. Eğer Task ise, sonuç elde edilir. Eğer hata varsa, hata await noktasında fırlatılır.
public async Task OrnekAsyncMetot()
{
Console.WriteLine("Async metot başladı.");
// VeriIndirAsync bir Task döndürür.
// Bu işlem zaman alabilir.
string indirilenVeri = await VeriIndirAsync("http://example.com"); // 1. await
// --- Kontrol buraya geldiğinde: ---
// 1. VeriIndirAsync tamamlanmış olacak.
// 2. indirilenVeri değişkeni sonucu içerecek.
// 3. Kontrolü çağıran metoda geri veren iş parçacığı,
// işlem tamamlandıktan sonra bu noktadan devam etmek üzere
// geri çağrılmış olabilir (veya işlem çok hızlı bittiyse hiç ayrılmamış olabilir).
Console.WriteLine($"Veri indirildi: {indirilenVeri.Length} karakter.");
// Başka bir asenkron işlem
await VeriKaydetAsync(); // 2. await (Task döndürüyor, sonuç yok)
// --- Kontrol buraya geldiğinde: ---
// 1. VeriKaydetAsync tamamlanmış olacak.
Console.WriteLine("Async metot bitti.");
return "İşlem Başarılı"; // Task olarak sarmalanır
}
public async Task CagiriciMetot()
{
Console.WriteLine("CagiriciMetot başladı.");
Task gorev = OrnekAsyncMetot(); // OrnekAsyncMetot hemen bir Task döndürür.
Console.WriteLine("OrnekAsyncMetot çağrıldı, Task alındı. Henüz bitmedi.");
// Burada başka işler yapılabilir...
Console.WriteLine("Başka işler yapılıyor...");
// Şimdi OrnekAsyncMetot'un bitmesini ve sonucunu bekleyelim.
string sonuc = await gorev; // gorev tamamlanana kadar bekler (bloklamadan!)
Console.WriteLine($"CagiriciMetot: OrnekAsyncMetot sonucu: {sonuc}");
Console.WriteLine("CagiriciMetot bitti.");
}
Bölüm 3: Nasıl Çalışır? Derleyici Sihirbazlığı ve SynchronizationContext

async/await sihirli gibi görünse de, aslında derleyicinin yaptığı akıllıca bir dönüşüme dayanır.

3.1. Durum Makinesi (State Machine)

Bir metodu async olarak işaretlediğinizde, derleyici metodu temel olarak bir durum makinesine dönüştürür. Bu durum makinesi, metodun farklı await noktalarındaki durumunu (hangi await’te beklendiği, yerel değişkenlerin değerleri vb.) takip eden bir yapıdır.

await ile karşılaşıldığında ve görev henüz tamamlanmadıysa:

Durum makinesi mevcut durumu (hangi satırda kalındığı, yerel değişkenler) kaydeder.
Kontrol çağıran metoda geri verilir.
Beklenen Task tamamlandığında, .NET runtime’ı durum makinesinin “devam et” metodunu çağırır.
Durum makinesi kaydedilen durumdan geri yüklenir ve kod await’in hemen sonrasından çalışmaya devam eder.
Bu karmaşık dönüşüm sayesinde, biz geliştiriciler asenkron kodu neredeyse senkron gibi yazabiliriz, ancak arka planda bloklama olmadan çalışır.

3.2. SynchronizationContext ve Görev Zamanlama

await’ten sonra devamlılığın nerede çalıştırılacağı önemli bir sorudur. Bu, genellikle SynchronizationContext.Current tarafından belirlenir.

UI Uygulamaları (WinForms, WPF, MAUI vb.): UI thread’inin kendine özgü bir SynchronizationContext’i vardır. await bir UI thread’inde başlatılırsa, varsayılan davranış, devamlılığın tekrar aynı UI thread’inde çalıştırılmak üzere zamanlanmasıdır. Bu, await’ten sonra UI elemanlarına güvenle erişebilmenizi sağlar, çünkü hala doğru thread’desinizdir.
ASP.NET Core: Her istek için bir SynchronizationContext oluşturulur (ancak bu, UI context’i gibi tek bir thread’e bağlı değildir). Devamlılıklar bu context üzerinde çalıştırılır, bu da HttpContext gibi isteğe bağlı verilere erişimi sürdürmeye yardımcı olur.
Konsol Uygulamaları ve Diğerleri: Varsayılan olarak bir SynchronizationContext bulunmaz (SynchronizationContext.Current null’dır). Bu durumda, devamlılık genellikle Task’ı tamamlayan iş parçacığında veya bir Thread Pool iş parçacığında çalıştırılır.
3.3. ConfigureAwait(false): Bağlamı Yakalamaktan Kaçınma

Varsayılan olarak await, mevcut SynchronizationContext’i (varsa) yakalar ve devamlılığı bu bağlamda çalıştırmaya çalışır. Ancak bu her zaman gerekli veya istenen bir durum değildir, özellikle kütüphane (library) kodu yazarken.

Eğer yazdığınız kod (örneğin, bir NuGet paketi içindeki bir metot) UI veya belirli bir ASP.NET context’ine bağımlı değilse, await’ten sonra orijinal context’e dönmeye çalışmak gereksiz bir yük getirebilir ve hatta deadlock (kilitlenme) potansiyeli yaratabilir.

Bu nedenle, genel amaçlı kütüphane kodunda, await edilen her Task üzerinde ConfigureAwait(false) kullanmak şiddetle tavsiye edilen bir en iyi pratiktir:

public async Task VeriIndirVeIsleAsync(string url)
{
HttpClient client = new HttpClient(); // HttpClient kullanmak daha iyidir
// Ağ isteği - Orijinal context'e dönmeye gerek yok
string data = await client.GetStringAsync(url).ConfigureAwait(false);
// Veriyi işleme (CPU-bound olabilir veya olmayabilir)
// Bu aşamada da orijinal context'e dönmeye gerek yoksa...
string result = await IslemYapAsync(data).ConfigureAwait(false);
return result;
}
private async Task IslemYapAsync(string input)
{
// Simülasyon
await Task.Delay(50).ConfigureAwait(false);
return $"İşlenmiş: {input.ToUpper()}";
}
ConfigureAwait(false) kullanımı, await’e şunu söyler: “Bu görev tamamlandığında, devamlılığı çalıştırmak için orijinal SynchronizationContext’e geri dönmeye çalışma. Müsait olan herhangi bir thread pool iş parçacığında çalıştırabilirsin.”

Uygulama Seviyesi Kod (UI, ASP.NET Controller): Genellikle UI elemanlarına veya HttpContext’e erişmeniz gereken son await’ten önce ConfigureAwait(false) kullanmaktan kaçınmanız gerekir, çünkü orijinal bağlama dönmeniz önemlidir. Ancak performansı optimize etmek veya potansiyel kilitlenmeleri önlemek için, bağlama geri dönmenin gerekmediği ara await’lerde ConfigureAwait(false) kullanmak faydalı olabilir.

Bölüm 4: Asenkron Metotların Geri Dönüş Tipleri

async metotlar birkaç farklı geri dönüş tipi kullanabilir:

Task: En yaygın kullanılan tiptir. Bir değer döndürmeyen asenkron bir işlemi temsil eder. Çağıran kod, işlemin tamamlanmasını (await), başarılı olup olmadığını veya hata verip vermediğini bekleyebilir.

public async Task DosyayiSilAsync(string path) { // ... dosya silme işlemi ... await Task.Delay(10); // Simülasyon }
Task: Bir değer döndüren asenkron işlemler için kullanılır. Çağıran kod, işlemin tamamlanmasını bekleyebilir ve await ile TResult türündeki sonucu alabilir.

public async Task SatirSayisiniOkuAsync(string path) { // ... dosya okuma ve sayma ... await Task.Delay(10); // Simülasyon return 150;
void (async void): Genellikle Kaçınılması Gerekir! async void metotlar, çağıran koda işlemin ne zaman bittiğini veya bir hata oluşup oluşmadığını bildirmenin standart bir yolunu sunmaz. await edilemezler.

Hata Yönetimi: async void bir metot içinde oluşan ve yakalanmayan bir istisna, doğrudan SynchronizationContext’e (varsa) veya ThreadPool’a gönderilir ve genellikle uygulamanın çökmesine neden olur. try-catch ile sarmalanması çok zordur.
Test Edilebilirlik: Test edilmesi zordur.
Ne Zaman Kullanılır? async void’un tek meşru kullanım alanı, olay yöneticileridir (event handlers), çünkü olay temsilcilerinin imzası genellikle void döndürür. Ancak bu durumda bile, async void metodun tüm içeriğini bir try-catch bloğu ile sarmalamak kritik öneme sahiptir.
// OLAY YÖNETİCİSİ - async void'un kabul edilebilir kullanımı private async void Button_Click(object sender, RoutedEventArgs e) { try { statusLabel.Text = "İşlem Başlatılıyor..."; string data = await VeriIndirAsync("http://example.com"); resultTextBox.Text = data; statusLabel.Text = "İşlem Tamamlandı."; } catch (Exception ex) { // Hata YAKALANMALI! statusLabel.Text = "Hata!"; MessageBox.Show($"Hata: {ex.Message}"); } }
ValueTask (ve ValueTask — C# 7.0+): Task bir referans türüdür (sınıf). Çok sık çağrılan ve sıklıkla senkron olarak tamamlanabilen asenkron metotlarda, her seferinde bir Task nesnesi oluşturmak bellek ayırma (allocation) maliyeti getirebilir. ValueTask bir değer türüdür (struct) ve bu ayırma maliyetini önleyebilir.
Eğer metot senkron olarak tamamlanırsa, sonucu doğrudan ValueTask içine sarmalar, ek bir nesne ayırmaz.
Eğer asenkron olarak tamamlanırsa, arka planda yine bir Task kullanır.
Kullanım Alanları: Performansın kritik olduğu ve metodun sık sık senkron olarak tamamlanma olasılığının yüksek olduğu durumlar (örneğin, önbelleğe alınmış veriyi döndürme).
Dikkat: ValueTask’ı Task’tan daha dikkatli kullanmak gerekir. Özellikle, bir ValueTask üzerinde birden fazla kez await yapmak veya .Result/.GetAwaiter().GetResult() çağırmak güvenli değildir (altta yatan nesne yeniden kullanılabilir). Genellikle hemen await edilmelidir.
private Dictionary _cache = new Dictionary(); public async ValueTask VeriAlVeyaIndirAsync(string url) { if (_cache.TryGetValue(url, out string cachedData)) { // Senkron tamamlanma - ValueTask sonucu doğrudan sarar. return cachedData; } else { // Asenkron tamamlama - Arka planda Task kullanılabilir. HttpClient client = new HttpClient(); string data = await client.GetStringAsync(url).ConfigureAwait(false); _cache[url] = data; // Önbelleğe ekle return data; } }
Bölüm 5: Yaygın Asenkron Desenler

5.1. G/Ç-Bağımlı (I/O-Bound) İşlemler

async/await’in en yaygın kullanım alanı budur. Ağ istekleri, dosya okuma/yazma, veritabanı sorguları gibi işlemler G/Ç-bağımlıdır. Bu işlemler sırasında CPU genellikle boştadır ve async/await iş parçacığının bu boşta bekleme süresinde başka işler yapmasını sağlar.

using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
public class DataOperations
{
public async Task WebVerisiIndirAsync(string url)
{
using (HttpClient client = new HttpClient())
{
// GetStringAsync I/O-bound bir asenkron işlemdir.
string content = await client.GetStringAsync(url).ConfigureAwait(false);
return content;
}
}
public async Task DosyayaYazAsync(string filePath, string content)
{
// StreamWriter.WriteAsync I/O-bound bir asenkron işlemdir.
// C# 8.0+ ile using bildirimi daha kısa olabilir: await using (...)
using (StreamWriter writer = File.CreateText(filePath))
{
await writer.WriteAsync(content).ConfigureAwait(false);
} // StreamWriter burada otomatik olarak Dispose edilir.
}
public async Task DosyadanOkuAsync(string filePath)
{
using (StreamReader reader = File.OpenText(filePath))
{
// ReadToEndAsync I/O-bound bir asenkron işlemdir.
string content = await reader.ReadToEndAsync().ConfigureAwait(false);
return content;
}
}
}
.NET Kütüphanelerindeki Asenkron Metotlar: .NET Base Class Library (BCL) içindeki birçok G/Ç işlemi yapan sınıfın artık …Async ile biten asenkron karşılıkları vardır (HttpClient, StreamReader, StreamWriter, SqlConnection, DbContext vb.). Senkron G/Ç metotları yerine her zaman bu asenkron metotları tercih edin.

5.2. CPU-Bağımlı (CPU-Bound) İşlemler

Karmaşık matematiksel hesaplamalar, görüntü işleme, veri sıkıştırma gibi işlemler CPU-bağımlıdır. Bu işlemler CPU’yu yoğun bir şekilde kullanır. Eğer böyle bir işlemi doğrudan UI thread’inde veya bir ASP.NET istek thread’inde async/await ile (ama G/Ç olmadan) yaparsanız, await edecek bir G/Ç noktası olmadığı için metot senkron gibi çalışır ve yine bloklamaya neden olur!

CPU-bağımlı uzun süren işleri asenkron hale getirmek ve UI/istek thread’ini bloke etmemek için bu işleri Task.Run kullanarak ayrı bir arka plan iş parçacığına (background thread) taşımamız gerekir. Task.Run, verilen işi bir Thread Pool iş parçacığında çalıştırır ve bu işlemi temsil eden bir Task döndürür.

public async Task GoruntuyuBulaniklastirAsync(Bitmap originalImage)
{
Console.WriteLine($"Bulanıklaştırma UI thread'inde mi? {!Thread.CurrentThread.IsThreadPoolThread}"); // Muhtemelen True
// Task.Run, verilen lambda ifadesini bir Thread Pool thread'inde çalıştırır.
Bitmap blurredImage = await Task.Run(() =>
{
// Bu kod artık bir Thread Pool thread'inde çalışıyor.
Console.WriteLine($"Lambda UI thread'inde mi? {!Thread.CurrentThread.IsThreadPoolThread}"); // Muhtemelen False
// --- Uzun süren CPU-yoğun bulanıklaştırma algoritması ---
// ... (Simülasyon) ...
Thread.Sleep(2000); // KÖTÜ PRATİK - Sadece simülasyon için! Gerçek CPU işi olmalı.
// --- ---
return ApplyBlurFilter(originalImage); // Varsayılan senkron metot
}).ConfigureAwait(false); // Geri UI context'ine dönmeye gerek yoksa
Console.WriteLine($"Bulanıklaştırma sonrası UI thread'inde mi? {!Thread.CurrentThread.IsThreadPoolThread}"); // ConfigureAwait(false) ise False, değilse True (UI'da)
return blurredImage;
}
private Bitmap ApplyBlurFilter(Bitmap img) { /* ... gerçek filtreleme kodu ... */ return img; }
Özetle:

G/Ç-Bağımlı: Doğrudan async/await kullanın (ReadFileAsync, GetStringAsync vb.). Task.Run GEREKMEZ.
CPU-Bağımlı: İşi Task.Run içine alın ve sonucu await ile bekleyin.
5.3. Birden Fazla Görevi Paralel Çalıştırma

Bazen birden fazla bağımsız asenkron işlemi aynı anda başlatıp hepsinin tamamlanmasını beklemek isteyebiliriz. Örneğin, birden fazla web servisinden veri çekmek. Bu işlemleri sırayla (await ile tek tek) yapmak yerine, Task.WhenAll kullanarak paralel çalıştırabiliriz.

public async Task> BirdenFazlaUrlIndirAsync(IEnumerable urls)
{
HttpClient client = new HttpClient();
var indirmeGorevleri = new List>>();
foreach (string url in urls)
{
// Görevi BAŞLAT ama hemen await ETME!
Task> gorev = Task.Run(async () =>
{
string data = await client.GetStringAsync(url).ConfigureAwait(false);
return new KeyValuePair(url, data);
});
indirmeGorevleri.Add(gorev);
}
// Tüm görevlerin tamamlanmasını BEKLE (paralel olarak çalışırlar)
KeyValuePair[] sonuclar = await Task.WhenAll(indirmeGorevleri).ConfigureAwait(false);
// Sonuçları bir dictionary'ye dönüştür
return sonuclar.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
Task.WhenAll: Verilen Task koleksiyonundaki tüm görevler tamamlandığında tamamlanan bir Task döndürür. Eğer görevler Task ise, Task.WhenAll bir Task döndürür ve sonuç dizisi, orijinal görevlerle aynı sırada sonuçları içerir. Eğer görevlerden herhangi biri hata verirse, Task.WhenAll tarafından döndürülen Task da hata durumuna geçer (genellikle ilk hatayı içerir, ancak AggregateException içinde tüm hatalar bulunabilir).
Task.WhenAny: Verilen Task koleksiyonundaki görevlerden herhangi biri tamamlandığında tamamlanan bir Task döndürür. Dönen Task, tamamlanan ilk görevin kendisidir. Hangi görevin önce biteceğini bilmediğimiz durumlarda veya zaman aşımları uygulamak için kullanılabilir.
5.4. İptal (Cancellation)

Uzun süren asenkron işlemlerin kullanıcı tarafından veya başka bir nedenle iptal edilebilmesi önemlidir. .NET bunun için CancellationTokenSource ve CancellationToken mekanizmasını sunar.

CancellationTokenSource Oluştur: İptal isteğini başlatacak olan nesnedir.
CancellationToken Al: CancellationTokenSource.Token özelliği, iptal durumunu dinlemek için asenkron metoda geçirilecek olan token’ı verir.
Token’ı Metoda Geçir: Asenkron metot (hem sizin yazdığınız hem de BCL’deki birçok …Async metodu) CancellationToken parametresi alacak şekilde tasarlanmalıdır.
İptali Kontrol Et: Asenkron metot içinde, periyodik olarak cancellationToken.IsCancellationRequested özelliğini kontrol edin veya cancellationToken.ThrowIfCancellationRequested() metodunu çağırarak iptal istendiyse bir OperationCanceledException fırlatılmasını sağlayın. BCL’deki birçok asenkron metot (HttpClient.GetAsync, Stream.ReadAsync vb.) CancellationToken’ı doğrudan destekler ve iptal istendiğinde kendiliğinden OperationCanceledException fırlatır.
İptali Başlat: İptal etmek istediğinizde CancellationTokenSource.Cancel() metodunu çağırın.
private CancellationTokenSource _cts;
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
startButton.IsEnabled = false;
cancelButton.IsEnabled = true;
_cts = new CancellationTokenSource(); // Yeni bir kaynak oluştur
CancellationToken token = _cts.Token;
try
{
statusLabel.Text = "Uzun işlem başlıyor...";
// Token'ı asenkron metoda geçir
int result = await UzunSurenIslemAsync(token);
statusLabel.Text = $"İşlem tamamlandı. Sonuç: {result}";
}
catch (OperationCanceledException) // İptal durumunu yakala
{
statusLabel.Text = "İşlem kullanıcı tarafından iptal edildi.";
}
catch (Exception ex)
{
statusLabel.Text = $"Hata: {ex.Message}";
}
finally
{
startButton.IsEnabled = true;
cancelButton.IsEnabled = false;
_cts.Dispose(); // Kaynağı serbest bırak
_cts = null;
}
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
_cts?.Cancel(); // İptal isteğini gönder
}
public async Task UzunSurenIslemAsync(CancellationToken cancellationToken)
{
int total = 0;
HttpClient client = new HttpClient(); // Örnek
for (int i = 0; i < 100; i++)
{
// 1. İptal istendi mi diye kontrol et (ThrowIf metodu)
cancellationToken.ThrowIfCancellationRequested();
// 2. Veya BCL metoduna token'ı geçir (varsa)
try
{
// GetAsync gibi metotlar iptal isteğini destekler
var response = await client.GetAsync($"http://example.com/data/{i}", cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
// ... response işleme ...
total++;
}
catch(HttpRequestException ex) { /* Hata yönetimi */ }
// İşlem parçası simülasyonu
// await Task.Delay(100, cancellationToken).ConfigureAwait(false); // Delay de iptali destekler
// 3. İptal istendi mi diye manuel kontrol et (özellikle CPU-bound döngülerde)
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("İptal isteği algılandı, çıkılıyor...");
// Temizlik işlemleri yapılabilir...
throw new OperationCanceledException(cancellationToken);
// veya break; ile döngüden çıkılabilir (ama OperationCanceledException daha standarttır)
}
total += i; // Örnek işlem
}
return total;
}
5.5. İlerleme Bildirimi (Progress Reporting)

Asenkron bir işlemin ilerlemesini (örneğin, bir dosya indirme yüzdesini) kullanıcı arayüzüne bildirmek için IProgress arayüzü ve Progress sınıfı kullanılır.

IProgress Parametresi: Asenkron metot, ilerleme verilerini (T türünde) kabul etmek için bir IProgress parametresi alır.
Progress Örneği Oluşturma: Çağıran kod (genellikle UI tarafı), bir Progress nesnesi oluşturur. Bu nesnenin yapıcısı (constructor) bir Action alır. Bu Action, ilerleme bildirildiğinde otomatik olarak Progress’nin oluşturulduğu SynchronizationContext üzerinde (yani genellikle UI thread’inde) çalıştırılır.
İlerlemeyi Bildirme: Asenkron metot, ilerleme kaydettiğinde progress?.Report(value) metodunu çağırır. Progress nesnesi, bu çağrıyı yakalar ve yapıcısında verilen Action’yi UI thread’inde tetikler.
// --- UI Tarafı (Çağıran Kod) ---
private async void StartWithProgressButton_Click(object sender, RoutedEventArgs e)
{
// İlerlemeyi UI'da gösterecek Action (int: yüzde değeri)
Action progressAction = (percentage) =>
{
progressBar.Value = percentage; // UI thread'inde güvenle güncellenir
statusLabel.Text = $"İndiriliyor: %{percentage}";
};
// Progress örneği oluştur, Action'ı ver
IProgress progressReporter = new Progress(progressAction);
try
{
startButton.IsEnabled = false;
// IProgress örneğini asenkron metoda geçir
string result = await DosyaIndirAsync("http://example.com/largefile.zip", "C:\downloads\file.zip", progressReporter);
statusLabel.Text = $"İndirme tamamlandı: {result}";
}
catch (Exception ex)
{
statusLabel.Text = $"Hata: {ex.Message}";
}
finally
{
startButton.IsEnabled = true;
progressBar.Value = 0;
}
}
// --- Asenkron Metot ---
public async Task DosyaIndirAsync(string url, string outputPath, IProgress progress = null)
{
using (HttpClient client = new HttpClient())
using (HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false)) // Sadece başlıkları oku
{
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
long totalBytesRead = 0;
byte[] buffer = new byte[8192];
int bytesRead;
await using (Stream contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
await using (FileStream fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.None, buffer.Length, true)) // useAsync: true
{
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (progress != null && totalBytes.HasValue)
{
// İlerlemeyi bildir
int percentage = (int)((double)totalBytesRead / totalBytes.Value * 100);
progress.Report(percentage); // Bu, UI thread'indeki Action'ı tetikler
}
}
}
return $"Dosya başarıyla indirildi: {outputPath}";
}
}
Bölüm 6: En İyi Pratikler ve Kaçınılması Gerekenler

Etkili ve hatasız asenkron kod yazmak için bazı önemli kurallara uymak gerekir:

Async All the Way Up: Eğer bir metot içinde await kullanıyorsanız, o metot da async olmalı ve genellikle Task veya Task döndürmelidir. Bu async çağrı zinciri, uygulamanızın giriş noktasına (örneğin, olay yöneticisi, controller action) kadar devam etmelidir. Asenkron bir metodu senkron bir metottan çağırmak ve sonucunu .Result veya .Wait() ile bloke ederek almak, deadlock’lara yol açabilir.
async void’dan Kaçının (Olay Yöneticileri Hariç): Daha önce belirtildiği gibi, async void hata yönetimi ve test edilebilirlik açısından problemlidir. Sadece olay yöneticilerinde kullanın ve içini mutlaka try-catch ile sarın.
ConfigureAwait(false)’i Akıllıca Kullanın: Kütüphane kodunda neredeyse her zaman ConfigureAwait(false) kullanın. Uygulama seviyesi kodda (UI, ASP.NET), bağlama (context) geri dönmenin gerekip gerekmediğini değerlendirin. Gerekmiyorsa (özellikle ara await’lerde) performansı artırmak ve potansiyel deadlock’ları önlemek için kullanın.
Asenkron Kod Üzerinde Bloklama Yapmayın (.Result, .Wait()): Asenkron bir görevin sonucunu almak için .Result özelliğini kullanmak veya .Wait() metodunu çağırmak, mevcut iş parçacığını bloke eder. Bu, asenkron programlamanın temel amacını ortadan kaldırır ve özellikle SynchronizationContext olan ortamlarda (UI, eski ASP.NET) kolayca deadlock’lara neden olabilir. Her zaman await kullanın.
Deadlock Senaryosu: UI thread’i await myAsyncTask.Result; çağırır. myAsyncTask arka planda çalışır ve tamamlandığında sonucunu UI thread’ine geri göndermeye çalışır (varsayılan await davranışı). Ancak UI thread’i .Result çağrısında bloke olduğu için sonucu alamaz. İki taraf da birbirini bekler -> Deadlock!
Task.Run’ı Sadece CPU-Bağımlı İşler İçin Kullanın: G/Ç-bağımlı işlemler için Task.Run kullanmak gereksizdir ve performansı düşürebilir (ekstra thread geçişleri). Doğrudan …Async metotlarını await edin.
İptali (Cancellation) Destekleyin: Uzun süren asenkron işlemlerin iptal edilebilir olması önemlidir. CancellationToken kullanın.
Hata Yönetimine Dikkat Edin: Asenkron metotlardaki hatalar Task nesnesi içinde saklanır ve await edildiğinde yeniden fırlatılır. try-catch bloklarını await ifadelerini içerecek şekilde kullanın. Task.WhenAll gibi durumlarda AggregateException oluşabileceğini unutmayın.
using ile Kaynakları Yönetin: HttpClient, Stream, SqlConnection gibi IDisposable kaynakları kullanan asenkron metotlarda using veya await using (C# 8.0+) ifadelerini kullanarak kaynakların düzgün bir şekilde serbest bırakıldığından emin olun.
ValueTask/ValueTask’ı Ölçerek Kullanın: Performansın çok kritik olduğu ve sık senkron tamamlanma beklenen durumlar dışında Task/Task genellikle yeterlidir. Gereksiz yere ValueTask karmaşıklığını eklemeyin.
Bölüm 7: Async Streams (C# 8.0 ve .NET Core 3.0+)

C# 8.0, zaman içinde birden fazla değer üreten asenkron dizilerle çalışmak için async streams özelliğini tanıttı. Bu, IAsyncEnumerable arayüzü ve await foreach döngüsü ile sağlanır.

Normalde, bir koleksiyon döndüren asenkron bir metot, tüm veriyi belleğe topladıktan sonra Task> gibi bir sonuç döndürürdü. Async streams ile, veriler üretildikçe (örneğin, veritabanından veya bir ağ akışından geldikçe) teker teker işlenebilir.

// Veri kaynağı (örneğin, veritabanından satır satır okuma simülasyonu)
public async IAsyncEnumerable VeriGetirAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();
// Simüle edilmiş asenkron işlem (her öğe için bekleme)
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
yield return $"Öğe {i}"; // Veriyi üret ve döngüye geri dön
}
}
// Kullanım
public async Task AsenkronAkislariIsleAsync()
{
Console.WriteLine("Asenkron akış başlıyor...");
await foreach (var item in VeriGetirAsync().WithCancellation(cancellationToken)) // WithCancellation C# 9.0+
{
// Her öğe geldiğinde bu blok çalışır
Console.WriteLine($"Alınan: {item}");
// Öğeyi işle...
}
Console.WriteLine("Asenkron akış tamamlandı.");
}
await foreach, IAsyncEnumerable’den bir sonraki öğeyi asenkron olarak bekler ve koleksiyon bitene kadar döngüyü sürdürür. Bu, büyük veri kümeleriyle bellek verimli bir şekilde çalışmak için çok kullanışlıdır.

Sonuç: Asenkron Programlamanın Gücü

C#’taki async ve await anahtar kelimeleri, asenkron programlamayı erişilebilir ve yönetilebilir hale getirmiştir. Bu model, modern uygulamaların temel gereksinimleri olan yanıt verme (responsiveness) ve ölçeklenebilirlik (scalability) için vazgeçilmezdir.

Doğru kullanıldığında async/await:

Kullanıcı arayüzlerinin akıcı kalmasını sağlar.
Sunucu uygulamalarının daha fazla isteği daha az kaynakla işlemesine olanak tanır.
Geliştiricilerin karmaşık asenkron senaryoları (paralel çalıştırma, iptal, ilerleme) daha temiz ve okunabilir bir kodla yönetmesini sağlar.
Ancak, async void kullanımı, .Result/.Wait() ile bloklama, ConfigureAwait(false)’in yanlış anlaşılması gibi potansiyel tuzaklara dikkat etmek önemlidir. En iyi pratikleri takip ederek ve asenkron akışı baştan sona düşünerek, async/await’in tüm avantajlarından yararlanabilir ve daha iyi performans gösteren, daha sağlam C# uygulamaları oluşturabilirsiniz. Asenkron programlama artık bir lüks değil, modern C# geliştirmenin bir standardıdır.

Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Özgeçmiş
Github
Github
Linkedin