Yıllardır kullandığımız Task.ConfigureAwait(false) artık tek başına yeterli değil. .NET 8 ile birlikte gelen yeni bir enum var ve bir Task tamamlandığında uygulamanın nasıl tepki vereceğini çok daha hassas bir şekilde belirlememize olanak tanıyor.
ConfigureAwait, .NET ekosisteminde asenkron programlamanın en kritik ve en çok yanlış anlaşılan kavramlarından biridir. Temel amacı bir await işleminden sonra kodun hangi context üzerinden devam edeceğini belirlemektir. Elbette işin bu kısmını tam olarak bilmediğimiz için true – false atamalarını da anlamıyoruz.
Birçok geliştirici ConfigureAwait(false) kullanarak sorunu çözdüğünü sanır. Ancak çağırdığınız kütüphane veya .NET’in kendi içindeki bir metot bunu kullanmayı unuttuysa uygulama yine kilitlenir.
ConfigureAwait(false) deadlock oluşturmamak için hiç iyi bir yöntem değildir. Çünkü böyle bir amacı da yok. Doğrudan bloklama (örneğin .Result veya GetResult() gibi) yaparken deadlock olmaması için, kütüphaneler ve runtime dahil olmak üzere tüm asenkron kodun ConfigureAwait(false) kullanması gerekir. Sizce bu ne kadar mümkün? Sürdürülebilir bir çözüm değil.

ConfigureAwait task üzerinde değil await için uygulanır.
Buradaki örneği inceleyin. Bu kullanımdaki ConfigureAwait(false) işe yaramaz. Çünkü ConfigureAwait(false) await için anlamlı ama await yok. Hatta senkron bloklama var, dolayısıyla kullanımı da hiçbir etki göstermez.
FetchAync().ConfigureAwait(false).GetAwaiter().GetResult();Burada ise ConfigureAwait(false) sonucu kullanılmıyor. “await task” yapıldığında varsayılan olarak ConfigureAwait(true) gelir.
var task = FetchAync();
task.ConfigureAwait(false);
await task;
// Doğrusu:
await FetchAync().ConfigureAwait(false);ConfigureAwaitOptions
ConfigureAwaitOptions, await edilebilir nesnelerin nasıl yapılandırılacağını belirleyen ve farklı yollar sunan yeni bir türdür. Task<T> üzerinde kullanımı mevcuttur. Bu seçenekler bir task tamamlandıktan sonra kodun nerede ve nasıl çalışmaya devam edeceğini belirtmenize olanak sağlar. Performans odaklı geliştiriciler için önemlidir.
public enum ConfigureAwaitOptions
{
None = 0x0,
ContinueOnCapturedContext = 0x1,
SuppressThrowing = 0x2,
ForceYielding = 0x4,
}None ve ContinueOnCapturedContext
“None” varsayılan davranıştır. Özel bir flag atanmadığını belirtir. Standart .ConfigureAwait(false) kullanımıyla benzer bir temel oluşturur ancak tek başına kullanıldığında sistemin varsayılan akışını takip eder.
“ContinueOnCapturedContext” ise geleneksel ConfigureAwait(true) ile aynıdır. İşlem bittiğinde, await edilen context için (SynchronizationContext veya TaskScheduler) geri dönülmesini zorunlu kılar. UI uygulamalarında arayüzü güncellemek için bu context kullanımı zorunlu hale gelir.
Örneğin bir domain sorgularken yeni bir thread başlatılır, bu flag kullanıldığında ve task işi tamamladığında UI’da başlatılan thread’e geri döndürüyoruz.
public async void OnSearchButtonClick(object sender, EventArgs e)
{
txtStatus.Text = "Searching";)
await CheckDomainAsync("recepserit.com").ConfigureAwait(ConfigureAwaitOptions.ContinueOnCapturedContext);
txtStatus.Text = "Available!";
}None kullanmış olsaydık kaldığı yerden herhangi bir thread üzerinde devam ederdi. Dolayısıyla bağlamı korumuş olduk.
ConfigureAwaitOptions.SuppressThrowing
Kritik bir seçenektir. Normalde bir await işlemi sırasında hata oluşursa bu hatanın çağrıldığı yere exception fırlatılır. Bu flag kullanıldığında Task hata ile sonuçlanırsa veya iptal edilirse, await satırında exception fırlatılmaz. Bunun yerine görev sessizce tamamlanır.
SuppressThrowing seçeneği, özellikle bir işlemin başarılı olup olmamasının kritik olmadığı veya hatayı Task nesnesi üzerinden manuel kontrol etmek istediğiniz durumlarda kodun okunabilirliğini artırır. Bunu daha iyi anlamak için bir senaryo hazırlayalım.
Geleneksek try-catch ile exception yakalama:
public async Task SaveAsync()
{
await SaveToDatabaseAsync();
try
{
await SendAnalyticsAsync();
}
catch (Exception ex)
{
Console.WriteLine(e.Message);
}
}Yeni yöntemle await satırı hata fırlatmayacağı için try-catch kalabalığından kurtulabiliriz. Hata fırlatılmaz ve sadece Task tamamlanır:
using System.Threading.Tasks;
public async Task SaveDataAsync()
{
await SaveToDatabaseAsync();
Task analyticsTask = SendAnalyticsAsync().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
await analyticsTask;
// Hata olmuşsa kontrol ediliyor
if (analyticsTask.IsFaulted)
{
Console.WriteLine($"Error: {analyticsTask.Exception?.InnerException?.Message}");
}
}Hata hala Task nesnesinin içindedir (IsFaulted true olur) ancak await operatörü bu hatayı çalışma zamanında bir Exception olarak fırlatmaz. Veritabanı kaydı veya ödeme alma gibi kritik işlemlerde asla kullanılmamalıdır çünkü işlemin başarısız olduğunu gözden kaçırabilirsiniz.
Domain sorgulama senaryosu SuppressThrowing kullanımı için harika bir örnektir. Özellikle bir kullanıcı bir alan adı aratırken her karakterde yeni bir istek atıldığında, eski istekleri iptal edip temiz bir başlangıç yapmak istersiniz.
Autocomplete için bir örnek:
private CancellationTokenSource? _cts;
private Task? _currentSearchTask;
public async Task OnSearchChangedAsync(string domainName)
{
_cts?.Cancel();
if (_currentSearchTask != null)
{
await _currentSearchTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
// Yeni bir iptal token ve task oluştur
_cts = new CancellationTokenSource();
// Yeni arama
_currentSearchTask = SearchAsync(domainName, _cts.Token);
await _currentSearchTask;
}
private async Task SearchAsync(string domain, CancellationToken token)
{
// API isteği veya veritabanı sorgusu
await Task.Delay(500, token);
Console.WriteLine($"{domain}");
}Ne yapmış olduk? Kullanıcı hızlıca “recep” yazarsa “r”, “re”, “rec” için ayrı ayrı görevler başlar. Her karakterde bir önceki iptal edilir. Tabi bu örneği daha geliştirebiliriz. Modern domain sorgulama sistemlerinde bu tür bir yaklaşım rate limit aşımına sebebiyet verir.
Bir metot Task döndürüyorsa, await işlemi sadece işin bitmesini bekler. İş bittiğinde geriye bir değer dönmez. SuppressThrowing kullanırsak exception fırlatmayacaktır. Ancak metot Task<T> döndürüyorsa await satırı bittiğinde mutlaka o tipten bir değer üretmek zorundadır. Sistem bu kararı yerinize veremediği için Task<T> ile bu flag kullanılmasına izin verilmez. Yani SuppressThrowing kullanamazsınız çünkü hata olursa sonuç ne olacak sorusu cevapsız kalır.
Şayet böyle bir kullanım yaparsanız “SuppressThrowing” kullandığınız satır CA2261 uyarısı verir ve uygulama çalışırken ArgumentOutOfRangeException fırlatır.
Task<bool> task = SearchAsync("example.com");
bool result = await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);CA2261: https://github.com/dotnet/roslyn-analyzers/pull/6669
Bir not: Normalde await kullanmadığınız zaman kodun durup o işin bitmesini beklemek için kullandığınız .GetAwaiter().GetResult() bloklama yöntemiyle Task içinde exception oluştuğunda anında fırlatılırr. SuppressThrowing ise artık bu andaki hatayı da bastırabiliyor.
saveTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing)
.GetAwaiter()
.GetResult();
Console.WriteLine("Task completed");Şimdi burada yeni olan şey nedir? Eski .NET sürümlerinde ConfigureAwait(false) sadece nerede devam edileceğini belirlerdi. .GetResult() çağırdığında hata varsa her türlü o hata fırlatılırdı. Yani try-catch yazmak zorundaydınız..NET 8 ile birlikte ConfigureAwait artık nasıl devam edeceğinizi değil bekleme şeklini de yapılandırıyor. Hataları dışarı sızdırmıyor.
ConfigureAwaitOptions.ForceYielding
Normalde bir Task zaten tamamlanmışsa await operatörü kodun geri kalanını senkron olarak çalıştırmaya devam eder. Bu flag kullanıldığında Task tamamlanmış olsa bile mevcut thread öğesinin serbest bırakılmasını ve yield yaparak asenkron olarak planlanmasını zorunlu kılar. ForceYielding seçeneği aslında asenkron metodun nezaketen thread kullanımını bırakmasını sağlar.
Normalde await edilen bir Task zaten tamamlanmışsa .NET runtime verimlilik adına kodun geri kalanını aynı thread üzerinde senkron olarak çalıştırmaya devam eder. Bu durum bazen uzun süren bir thread varsa UI ekranının kilitlenmesine sebebiyet verebilir.
using System.Threading.Tasks;
public async Task ProcessDataAsync()
{
for (int i = 0; i < 1000000; i++)
{
DoLightWork(i);
// ForceYielding sayesinde burada durup thread işlemini diğer işlere (UI güncelleme gibi) devreder.
if (i % 1000 == 0)
{
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
UpdateStatusText($"İşlem: %{i / 10000}");
}
}
}Burada Task bitmiş olsa bile çalışma zamanı sanki işlem devam ediyormuş gibi davranır. Çok hızlı tamamlanan ama binlerce kez tekrarlanan işlemleriniz varsa kullanmanız daha iyi olabilir.
Bu özellik en çok cancellation işlemleriyle birlikte kullanışlı olabilir. Bazı durumlarda kodun bir görevi iptal etmesi ve ardından yeni bir görev başlatmadan önce mevcut görevin tamamen bitmesini beklemesi gerekir.
ForceYielding | SuppressThrowing
Birlikte kullanım örneği yapalım.
Bir uygulamanın geçici dosyalarını silen metot geliştirelim. Bu dosyalar hatalara sebebiyet verebilir. Bu flag örneğini kullanarak deneme yapalım.
ForceYielding kullanarak: Her dosyada kullanılan thread serbest bırakılmalı ki UI’da bir donma olmasın.
SuppressThrowing kullanarak: Dosya kullanımda olsa bile hata fırlatmaz ve bir sonrakine geçer.
using System.Threading.Tasks;
using System.IO;
public async Task RunBackgroundCleanupAsync(string folderPath)
{
var files = Directory.GetFiles(folderPath);
foreach (var file in files)
{
await DeleteFileAsync(file).ConfigureAwait(
ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.SuppressThrowing
);
// Devam eder
}
}
private async Task DeleteFileAsync(string filePath)
{
File.Delete(filePath);
await Task.CompletedTask;
}.NET 8 ile hayatımıza giren ConfigureAwaitOptions bu kısıtlı alanı genişleterek geliştiricilere asenkron görevlerin yaşam döngüsü üzerinde tam bir hakimiyet sunuyor. Artık sadece kodun hangi thread üzerinde devam edeceğini değil, onun akışını da kontrol edebiliyoruz.



Yorum bırakın