- G/Ç ağırlıklı ve işlemci ağırlıklı işler için doğru aracı seçmek
- Bir
IAsyncEnumerable<T>yazmak, onuawait foreachile okumak ve işiCancellationTokenile durdurmak - Hesaplamaları
Parallelve 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ü | Örnek | Araç |
|---|---|---|
| G/Ç: bekleme | HTTP isteği, dosya, SQL | async/await, Task.WhenAll |
| Parça parça gelen veri | sayfalı bir API, büyük bir dosya | IAsyncEnumerable<T>, await foreach |
| İşlemci: hesaplama | asal sayılar, raporlar, görüntüler | Parallel.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.
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;
}
}Received 95 Received 78 Received 88 80 and above: 95, 88
İ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.
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");
}
}Part 1 downloaded Cancelled: it took too long
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.
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}";
}Timeout: showing yesterday's cached results
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.
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;
}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
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 500500long 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.
// 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);
}ConfigureAwait(false) kullanır// blocks the thread while waiting;
// in a UI app (WPF, WinForms) this can freeze or deadlock
string report = LoadReportAsync().Result;// the thread is free while waiting
string report = await LoadReportAsync();Önemli noktalar
- G/Ç işleri için
async/await, işlemci işleri içinParallelve PLINQ. IAsyncEnumerable<T>+await foreachelemanları geldikçe işler; .NET 10'da bunlar için LINQ vardır.CancellationTokenzincir boyunca aktarılır; iptaldeOperationCanceledExceptionfırlatılır, süre sınırı içinWaitAsyncvardır.- Paralel kodda her iş parçacığı kendi hücresine yazar ya da
Interlockedkullanır. - “Sonuna kadar async”:
.Resultve.Wait()yok; kütüphane kodundaConfigureAwait(false).
Kendini test et
10 soru. Her doğru cevap XP kazandırır.