İçeriğe geç
Educora
İleri23 dk14 / 16

Asenkron ve paralel programlama

Bekleme gerektiren işi hesaplama gerektiren işten ayır: asenkron akışlar, iptal belirteçleri, `Parallel` ve PLINQ, `ConfigureAwait` ve engelleme tuzakları.

Kendini test et
Bu derste öğreneceklerin
  • G/Ç ağırlıklı ve işlemci ağırlıklı işler için doğru aracı seçmek
  • Bir IAsyncEnumerable<T> yazmak, onu await foreach ile okumak ve işi CancellationToken ile durdurmak
  • Hesaplamaları Parallel ve PLINQ ile paralelleştirmek ve engelleme tuzaklarından kaçınmak

Önceki modülde bekleme gerektiren işleri async/await ve Task.WhenAll ile yapmayı öğrendin. Şimdi iki farklı sorunu ayırma zamanı. G/Ç ağırlıklı işler (ağ, dosyalar, veritabanları) zamanlarının çoğunu beklemeyle geçirir; bunlar için async/await gerekir. İşlemci ağırlıklı işler (hesaplamalar, görüntü işleme) işlemciyi meşgul eder; onları çekirdekler arasında bölmek için Parallel ve PLINQ vardır. Bu derste ikisini de, ayrıca asenkron akışları ve iptali öğreneceksin.

İşin türüÖrnekAraç
G/Ç: beklemeHTTP isteği, dosya, SQLasync/await, Task.WhenAll
Parça parça gelen verisayfalı bir API, büyük bir dosyaIAsyncEnumerable<T>, await foreach
İşlemci: hesaplamaasal sayılar, raporlar, görüntülerParallel.For, AsParallel()
Uzun ya da artık gereksiz işkullanıcı “İptal” düğmesine bastıCancellationToken

Asenkron akışlar: IAsyncEnumerable

Bir Task<List<T>>, sonucu ancak bütün liste hazır olduğunda verir. Veri parça parça geliyorsa (sayfalı bir API, büyük bir dosya), her elemanı gelir gelmez işlemek daha iyidir. Bir asenkron akış, yani IAsyncEnumerable<T>, yield return'ü await ile birleştirir; tüketici ise onu await foreach ile okur. .NET 10'da LINQ metotları (Where, Select, ToListAsync) asenkron akışlar için de ek paket olmadan kullanılabilir.

C#
await foreach (int score in LoadScoresAsync())
    Console.WriteLine($"Received {score}");

List<int> high = await LoadScoresAsync().Where(s => s >= 80).ToListAsync();
Console.WriteLine($"80 and above: {string.Join(", ", high)}");

async IAsyncEnumerable<int> LoadScoresAsync()
{
    int[] fromServer = [95, 78, 88];
    foreach (int score in fromServer)
    {
        await Task.Delay(200);            // each item arrives from the network
        yield return score;
    }
}
Beklenen çıktı
Received 95
Received 78
Received 88
80 and above: 95, 88
Her puan gelir gelmez yazdırılır; ikinci bölümde asenkron akışa LINQ uygulandı (.NET 10)

İptal: CancellationToken

Kullanıcı sayfayı kapattı ama sunucu hâlâ raporu mu hazırlıyor? Uzun bir iş durdurulabilmelidir. CancellationTokenSource iptal sinyalini oluşturur, onun Token'ı ise zincir boyunca tüm asenkron metotlara aktarılır. CancelAfter bir zamanlayıcı kurar; Task.Delay, HttpClient ve EF Core gibi .NET metotları belirteci kabul eder ve iptal edildiğinde OperationCanceledException fırlatır. Kendi hesaplama döngünde ise token.ThrowIfCancellationRequested() çağır.

C#
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMilliseconds(450));

try
{
    await DownloadAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled: it took too long");
}

async Task DownloadAsync(CancellationToken token)
{
    for (int part = 1; part <= 5; part++)
    {
        await Task.Delay(300, token);     // throws once the token is cancelled
        Console.WriteLine($"Part {part} downloaded");
    }
}
Beklenen çıktı
Part 1 downloaded
Cancelled: it took too long
450 ms'de iptal: ilk parça (300 ms) indirildi, ikincisi yarıda kaldı

Basit bir süre sınırı için görevin kendisini değiştirmen gerekmez: WaitAsync(TimeSpan) (.NET 6'dan beri), sonuç zamanında gelmezse TimeoutException fırlatır. Dikkat: asıl iş arka planda sürer; onu gerçekten durdurmak için bir belirteç vermek gerekir.

C#
Task<string> answer = FetchAsync("exam results", TimeSpan.FromSeconds(2));

try
{
    Console.WriteLine(await answer.WaitAsync(TimeSpan.FromMilliseconds(500)));
}
catch (TimeoutException)
{
    Console.WriteLine("Timeout: showing yesterday's cached results");
}

async Task<string> FetchAsync(string what, TimeSpan delay)
{
    await Task.Delay(delay);
    return $"Fresh {what}";
}
Beklenen çıktı
Timeout: showing yesterday's cached results
Sunucu 2 saniye sürüyor, biz ise yalnızca 0,5 saniye bekliyoruz

Paralel hesaplama: Parallel ve PLINQ

Parallel.For ve Parallel.ForEach, bir döngünün yinelemelerini havuzdaki iş parçacıkları arasında böler; AsParallel() ise sıradan bir LINQ sorgusunu PLINQ'e, yani paralel LINQ'e dönüştürür. Aşağıda dört ağır hesaplama (farklı sınırlara kadar asal sayıları saymak) aynı anda yürüyor. Ana kural: her iş parçacığı ya kendi hücresine yazar (results[i]) ya da paylaşılan değişkeni Interlocked ile atomik olarak değiştirir.

C#
using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

int[] limits = [1_000_000, 2_000_000, 3_000_000, 4_000_000];
int[] results = new int[limits.Length];
Parallel.For(0, limits.Length, i => results[i] = CountPrimes(limits[i]));   // each task writes its own cell
for (int i = 0; i < limits.Length; i++)
    Console.WriteLine($"Primes below {limits[i]:N0}: {results[i]:N0}");

long total = 0;
Parallel.ForEach(Enumerable.Range(1, 1000), n => Interlocked.Add(ref total, n));
Console.WriteLine($"Sum 1..1000 = {total}");

int evenSquares = Enumerable.Range(1, 1000).AsParallel().Count(n => n * n % 2 == 0);
Console.WriteLine($"PLINQ, even squares: {evenSquares}");

static int CountPrimes(int limit)
{
    int count = 0;
    for (int n = 2; n < limit; n++)
    {
        bool prime = true;
        for (int d = 2; d * d <= n; d++)
            if (n % d == 0) { prime = false; break; }
        if (prime) count++;
    }
    return count;
}
Beklenen çıktı
Primes below 1,000,000: 78,498
Primes below 2,000,000: 148,933
Primes below 3,000,000: 216,816
Primes below 4,000,000: 283,146
Sum 1..1000 = 500500
PLINQ, even squares: 500
Dört hesaplama paralel yürür ama sonuçlar hep aynı sırayla yazdırılır, çünkü her birinin kendi hücresi vardır
Yarış durumu
long total = 0;
Parallel.ForEach(Enumerable.Range(1, 1000), n =>
{
    total += n;      // read + add + write from many threads: a race
});
// the result can be less than 500500
Atomik toplama
long total = 0;
Parallel.ForEach(Enumerable.Range(1, 1000), n =>
{
    Interlocked.Add(ref total, n);   // one atomic operation
});
// always 500500 (or simply: Enumerable.Range(1, 1000).AsParallel().Sum())

Peki işlemci işi asenkron koda nasıl bağlanır? Grafik arayüzde ağır bir hesaplamayı await Task.Run(() => CountPrimes(4_000_000)) ile havuzdaki bir iş parçacığına gönder: pencere donmaz, sonuç hazır olduğunda gelir. Sunucuda (ASP.NET Core) ise Task.Run genellikle hiçbir şey kazandırmaz; iş yalnızca bir havuz iş parçacığından diğerine geçer. Task.Run tek bir işi arka plana atmak, Parallel ise büyük bir işi parçalara bölmek içindir.

ConfigureAwait ve engelleme tuzakları

Grafik arayüzlü uygulamalarda (WPF, WinForms, MAUI) bir eşitleme bağlamı vardır: await'ten sonra kod, düğmeleri ve metinleri güvenle değiştirebilmek için arayüz iş parçacığına geri döner. Kütüphane kodu ise arayüzle çalışmaz; bu yüzden orada ConfigureAwait(false) yazılır: “devamı herhangi bir iş parçacığında çalıştır”. Bu biraz daha hızlıdır ve bazı kilitlenmeleri (deadlock) önler. ASP.NET Core'da ve konsol uygulamalarında böyle bir bağlam yoktur; bu yüzden uygulama kodunda genellikle ConfigureAwait gerekmez.

C#
// library code: it does not need to come back to the caller's UI thread
public static async Task<int> CountLinesAsync(string path, CancellationToken token = default)
{
    string[] lines = await File.ReadAllLinesAsync(path, token).ConfigureAwait(false);
    return lines.Count(line => line.Length > 0);
}
Bir kütüphane metodu: belirteç kabul eder ve ConfigureAwait(false) kullanır
Yanlış: `.Result` ile engelleme
// blocks the thread while waiting;
// in a UI app (WPF, WinForms) this can freeze or deadlock
string report = LoadReportAsync().Result;
Doğru: `await`
// the thread is free while waiting
string report = await LoadReportAsync();

Önemli noktalar

  • G/Ç işleri için async/await, işlemci işleri için Parallel ve PLINQ.
  • IAsyncEnumerable<T> + await foreach elemanları geldikçe işler; .NET 10'da bunlar için LINQ vardır.
  • CancellationToken zincir boyunca aktarılır; iptalde OperationCanceledException fırlatılır, süre sınırı için WaitAsync vardır.
  • Paralel kodda her iş parçacığı kendi hücresine yazar ya da Interlocked kullanır.
  • “Sonuna kadar async”: .Result ve .Wait() yok; kütüphane kodunda ConfigureAwait(false).

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Tüm çekirdeklerde asal sayı saymak için hangi araç uygundur?