C# Async/Await: En Tehlikeli 5 Tuzak

async/await kod okunurluğunu artırsa da, yanlış kullanımı uygulamayı dondurabilir (Deadlock), performansı yerle bir edebilir veya sessizce hata yutmasına (Silent Failure) neden olabilir. Bu yazıda; SyncContext kaynaklı Deadlock’lar, ConfigureAwait(false)’ın hayati önemi, async void’in neden kötü olduğu, Fire-and-Forget fiyaskoları ve using bloğu ile yaşanan zamanlama sorunları ele alınmaktadır.

C# Async/Await: En Tehlikeli 5 Tuzak

C# Async/Await: En Tehlikeli 5 Tuzak

Async/Await, C# dilinin en sevilen özelliklerinden biridir ancak "kullanması kolay" olması, "doğru kullanmayı" garanti etmez. İşte profesyonel projelerde en sık karşılaşılan 5 ölümcül tuzak ve çözümleri:

Tuzak 1: Senkronizasyon Bağlamı Nedeniyle Deadlock (Kilitlenme)

  • Sorun: UI (WPF/WinForms) veya ASP.NET (Eski) uygulamalarında bir SynchronizationContext vardır. Eğer bir async metodu .Result veya .Wait() ile senkron olarak çağırırsanız, thread Task tamamlanana kadar bloke olur. Ancak Task tamamlandığında devam etmek için yakalanan context'e (UI thread) ihtiyaç duyar. UI thread zaten bloke olduğu için karşılıklı bekleme (Deadlock) oluşur.

  • Çözüm: Mümkünse "Async all the way" (yukarıdan aşağıya asenkron) prensibine uyun. Zorunlu kalınırsa .ConfigureAwait(false) kullanın ama en iyisi asla .Result veya .Wait() kullanmayın; bunun yerine await kullanın.

Tuzak 2: ConfigureAwait(false)'ı İhmal Etmek (Performans ve Kilit Riski)

  • Sorun: Varsayılan davranışta await'den sonraki kod, orijinal context'te (UI veya Request thread) çalışmak ister. Bu, gereksiz bir iş parçacığı geçişine (Thread Switch) neden olur ve performansı düşürür. Ayrıca yukarıdaki Deadlock riskini artırır.

  • Çözüm: UI ile etkileşime girmeyen (örneğin alttaki katmanlar, veritabanı veya HTTP çağrıları) tüm await'lerde .ConfigureAwait(false) kullanın.

    csharp

    await httpClient.GetStringAsync(url).ConfigureAwait(false);

Tuzak 3: async void Kullanmak (En Büyük Günah)

  • Sorun: async void metotlar içinde fırlatılan hatalar, çağıran tarafından yakalanamaz (try-catch çalışmaz) ve doğrudan uygulamayı çöktürür (Application Crash). Ayrıca geri dönüş tipi olmadığı için ne zaman bittiğini takip edemezsiniz.

  • Çözüm: Sadece UI event handler'ları (Button_Click) için async void kullanın. Bunun dışındaki TÜM durumlarda Task veya Task<T> döndürün.

Tuzak 4: "Fire and Forget" (Ateşle ve Unut) Fiyaskosu

  • Sorun: Arka planda çalışan bir işlemi beklememek için bazen _ = MyMethodAsync(); yazılır. Ancak bu metodun içinde bir hata oluşursa, bu hata sessizce yutulur (Silent Failure). Log kaydı bile kalmaz ve uygulama hatalı duruma geçer.

  • Çözüm: Eğer gerçekten fire-and-forget yapacaksanız, mutlaka bir exception handling mekanizması ekleyin.

    csharp

    _ = Task.Run(async () => {
        try { await MyMethodAsync(); } 
        catch (Exception ex) { Logger.Log(ex); }
    });

    (Not: .NET 6+ arka plan hizmetleri için BackgroundService veya IHostedService daha güvenlidir.)

Tuzak 5: using ve Async/Zamanlama Karmaşası

  • Sorun: Bir HttpClient veya DbContext nesnesini using bloğu içinde yakalayıp içinde await yaptığınızda, metodun içinde işlem devam ederken Dispose çağrılabilir. Bu durum, "ObjectDisposedException" fırlatılmasına yol açar.

  • Çözüm: using'in kapsamını (scope) doğru ayarlayın. Eğer nesneyi metot dışına taşıyacaksanız, using kullanmayın, üst seviye (örn. constructor'dan alınan) bir bağımlılık olarak yönetin (Dependency Injection ile).


Ekstra Bonus Tuzak - ValueTask Performansı:
Çok sık çağrılan ve genelde senkron sonuç döndüren metotlar için Task yerine ValueTask kullanmak heap tahsisini (alloc) azaltır. Ancak ValueTask'i iki kere await yapmayın veya .Result ile çağırmayın, aksi takdirde hata alırsınız.

Sonuç: Async/Await güçlüdür ama kuralları vardır. "Async all the way", "ConfigureAwait(false) bilinci" ve "asla async void" kurallarını hayat felsefesi haline getirirseniz, uygulamalarınız hem hızlı hem de kararlı çalışacaktır.

Tüm yazılar