C#'ın async/await mekanizması ilk bakışta oldukça sezgisel görünür: metodun önüne async yaz, bekleme noktasına await koy, bitti. Ama bu sözdiziminin arkasında derleyicinin ürettiği durum makinesi, SynchronizationContext yönetimi ve ince hatalar sizi bekliyor.

Derleyici Perde Arkasında Ne Yapıyor?

Her async metod, derleyici tarafından bir state machine'e dönüştürülür. Şu kodu yazarsanız:

public async Task<string> GetDataAsync(int id)
{
    var user = await _userRepo.FindAsync(id);
    var data = await _dataRepo.GetAsync(user.DataId);
    return data.Content;
}

Derleyici bunu kabaca şuna çevirir:

// Derleyicinin ürettiği yapının basitleştirilmiş hali
private struct GetDataAsyncStateMachine : IAsyncStateMachine
{
    public int _state; // -1=başlamadı, 0=ilk await, 1=ikinci await, -2=bitti
    public AsyncTaskMethodBuilder<string> _builder;
    private Task<User?> _userTask;
    private Task<Data>  _dataTask;
    private User? _user;

    public void MoveNext()
    {
        switch (_state)
        {
            case -1:
                _userTask = _userRepo.FindAsync(id);
                if (!_userTask.IsCompleted) { _state = 0; /* suspend */ return; }
                goto case 0;
            case 0:
                _user = _userTask.Result;
                _dataTask = _dataRepo.GetAsync(_user.DataId);
                if (!_dataTask.IsCompleted) { _state = 1; return; }
                goto case 1;
            case 1:
                _builder.SetResult(_dataTask.Result.Content);
                break;
        }
    }
}

Bunun pratikte önemi: await edilen işlem zaten tamamlanmışsa (IsCompleted == true), state machine hiç suspend edilmez ve senkron devam eder. Bu yüzden tamamlanmış Task döndürmek gerçekten ücretsizdir.

SynchronizationContext ve Deadlock Tuzağı

ASP.NET Core'da SynchronizationContext yoktur, bu yüzden deadlock nadirdir. Ama WPF, WinForms ve klasik ASP.NET'te şu klasik tuzak sizi bekler:

// Deadlock örneği — UI veya klasik ASP.NET'te
public string GetData()
{
    // .Result veya .Wait() asla kullanmayın!
    return GetDataAsync().Result; // DEADLOCK
}

public async Task<string> GetDataAsync()
{
    await Task.Delay(100); // SynchronizationContext'e geri dönmeye çalışır
    return "data";        // ama .Result onu blokluyor → kilitlenme
}

// Çözüm 1: Sonuna kadar async kalın
public async Task<string> GetDataAsync() { ... }

// Çözüm 2: ConfigureAwait(false) — UI context'e geri dönme
public async Task<string> GetDataAsync()
{
    await Task.Delay(100).ConfigureAwait(false); // Context'e dönme
    return "data";
}

ConfigureAwait(false) Ne Zaman Kullanılmalı?

Kural basit: kütüphane kodunda her zaman, uygulama kodunda genellikle gerekmez.

// Kütüphane kodu — UI thread'ine bağımlılık yok
public async Task<byte[]> ReadFileAsync(string path)
{
    using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
                                  bufferSize: 4096, useAsync: true);
    var buffer = new byte[fs.Length];
    await fs.ReadAsync(buffer).ConfigureAwait(false); // Hangi thread'de devam ederse etsin
    return buffer;
}

// ASP.NET Core uygulama kodu — ConfigureAwait(false) gerekmez
public async Task<IActionResult> GetAsync(int id)
{
    var data = await _service.GetAsync(id); // Context zaten yok, fark etmez
    return Ok(data);
}

Task vs ValueTask

Task<T> her zaman heap'te bir nesne oluşturur. Metodunuz çoğu zaman senkron tamamlanıyorsa bu gereksiz GC baskısı yaratır. İşte burada ValueTask<T> devreye girer:

// Task: Her çağrıda heap allocation
public async Task<int> GetCachedCountAsync()
{
    if (_cache.TryGetValue("count", out int cached))
        return cached; // Yine de Task nesnesi oluşturulur!

    var count = await _db.Items.CountAsync();
    _cache.Set("count", count);
    return count;
}

// ValueTask: Senkron path'de sıfır allocation
public ValueTask<int> GetCachedCountAsync()
{
    if (_cache.TryGetValue("count", out int cached))
        return ValueTask.FromResult(cached); // Stack'te kalır, heap allocation yok

    return new ValueTask<int>(FetchAndCacheAsync());
}

private async Task<int> FetchAndCacheAsync()
{
    var count = await _db.Items.CountAsync();
    _cache.Set("count", count);
    return count;
}

ValueTask Kullanım Kuralları

  • Bir ValueTask yalnızca bir kez await edilebilir
  • .Result sadece tamamlanmışsa kullanılabilir
  • Performans kritik, sıklıkla senkron tamamlanan metodlarda tercih edin
  • Eğer method her zaman async ise Task kullanın — ValueTask değil

CancellationToken: Doğru Kullanım

// CancellationToken her async metodun son parametresi olmalı
public async Task<List<Order>> GetOrdersAsync(
    int customerId,
    CancellationToken ct = default)
{
    // DB sorgusuna ilet
    return await _db.Orders
        .Where(o => o.CustomerId == customerId)
        .ToListAsync(ct); // EF Core token'ı destekler

    // HTTP çağrısına ilet
    var response = await _httpClient.GetAsync($"/orders/{customerId}", ct);
}

// İptal kontrolü
public async Task ProcessLargeJobAsync(CancellationToken ct)
{
    foreach (var item in items)
    {
        ct.ThrowIfCancellationRequested(); // İptal istendiyse OperationCanceledException fırlatır

        await ProcessItemAsync(item, ct);
    }
}

// Zaman aşımı ile birlikte
public async Task<Data> GetWithTimeoutAsync(int id)
{
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
    try
    {
        return await GetDataAsync(id, cts.Token);
    }
    catch (OperationCanceledException)
    {
        throw new TimeoutException($"İstek {id} için zaman aşımına uğradı");
    }
}

async void: Sadece Event Handler'larda

// Kötü: Exception yakalanmaz, uygulama çöker
public async void LoadData()
{
    var data = await _service.GetAsync(); // Hata fırlarsa kimse yakalamaz
    Display(data);
}

// İyi: async Task
public async Task LoadDataAsync()
{
    var data = await _service.GetAsync();
    Display(data);
}

// async void sadece event handler'da kabul edilebilir
private async void Button_Click(object sender, EventArgs e)
{
    try
    {
        await LoadDataAsync(); // İçeride try-catch ile sarın
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

Paralel Async İşlemler

// Kötü: Sıralı — toplam süre = A + B + C
var a = await GetAAsync();
var b = await GetBAsync();
var c = await GetCAsync();

// İyi: Paralel — toplam süre = max(A, B, C)
var taskA = GetAAsync();
var taskB = GetBAsync();
var taskC = GetCAsync();
await Task.WhenAll(taskA, taskB, taskC);
var (a, b, c) = (taskA.Result, taskB.Result, taskC.Result);

// Sonuçlarla birlikte
var results = await Task.WhenAll(
    GetAAsync(),
    GetBAsync(),
    GetCAsync());

// İlk tamamlananı al
var first = await Task.WhenAny(GetFromServer1Async(), GetFromServer2Async());
var data  = await first; // Sonucu al

async/await'i doğru kullanmak performans ve güvenilirlik açısından ciddi fark yaratır. Her async metodun sonuna CancellationToken eklemek, kütüphane kodunda ConfigureAwait(false) kullanmak ve async void'den kaçınmak bu alandaki en önemli kurallar.