- I/O və CPU işləri üçün düzgün aləti seçmək
IAsyncEnumerable<T>yazmaq vəawait foreachilə oxumaq, işiCancellationTokenilə dayandırmaq- Hesablamaları
Parallelvə PLINQ ilə paralelləşdirmək və bloklama tələlərindən qaçmaq
Əvvəlki modulda async/await və Task.WhenAll ilə gözləmə tələb edən işləri görməyi öyrəndin. İndi iki fərqli problemi ayırmaq vaxtıdır. I/O işləri (şəbəkə, fayl, verilənlər bazası) vaxtının çoxunu gözləməyə sərf edir — onlar üçün async/await lazımdır. CPU işləri (hesablamalar, şəkil emalı) prosessoru yükləyir — onları nüvələr arasında bölmək üçün Parallel və PLINQ var. Bu dərsdə hər ikisini, həmçinin asinxron axınları və ləğvetməni öyrənəcəksən.
| İşin növü | Nümunə | Alət |
|---|---|---|
| I/O: gözləmə | HTTP sorğusu, fayl, SQL | async/await, Task.WhenAll |
| Hissə-hissə gələn verilənlər | səhifələnmiş API, böyük fayl | IAsyncEnumerable<T>, await foreach |
| CPU: hesablama | sadə ədədlər, hesabatlar, şəkillər | Parallel.For, AsParallel() |
| Uzun və ya artıq lazım olmayan iş | istifadəçi «Ləğv et» düyməsini basdı | CancellationToken |
Asinxron axınlar: IAsyncEnumerable
Task<List<T>> nəticəni yalnız bütün siyahı hazır olanda verir. Verilənlər hissə-hissə gəlirsə (səhifələnmiş API, böyük fayl), hər elementi gələn kimi emal etmək daha yaxşıdır. Asinxron axın — IAsyncEnumerable<T> — yield return-ü await ilə birləşdirir, istehlakçı isə onu await foreach ilə oxuyur. .NET 10-da LINQ metodları (Where, Select, ToListAsync) asinxron axınlar üçün də əlavə paket olmadan mövcuddur.
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
Ləğvetmə: CancellationToken
İstifadəçi səhifəni bağladı, server isə hələ də hesabat hazırlayır? Uzun iş dayandırıla bilməlidir. CancellationTokenSource ləğvetmə siqnalını yaradır, onun Token-i isə zəncir boyu bütün asinxron metodlara ötürülür. CancelAfter taymer qurur; Task.Delay, HttpClient, EF Core kimi .NET metodları token-i qəbul edir və ləğv olunanda OperationCanceledException atır. Öz hesablama dövründə isə 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
Sadə vaxt həddi üçün tapşırığın özünü dəyişmək lazım deyil: WaitAsync(TimeSpan) (.NET 6-dan) nəticə göstərilən vaxtda gəlməsə, TimeoutException atır. Diqqət: əsl iş fonda davam edir — onu həqiqətən dayandırmaq üçün token ötürmək lazımdır.
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 hesablamalar: Parallel və PLINQ
Parallel.For və Parallel.ForEach dövrün iterasiyalarını hovuzdakı thread-lər arasında bölür, AsParallel() isə adi LINQ sorğusunu PLINQ-ə — paralel LINQ-ə çevirir. Aşağıda dörd ağır hesablama — müxtəlif həddə qədər sadə ədədlərin sayı — eyni vaxtda gedir. Əsas qayda: hər thread ya öz xanasına yazmalı (results[i]), ya da ortaq dəyişəni Interlocked ilə atomik dəyişməlidir.
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())Bəs CPU işi asinxron koda necə qoşulur? Qrafik interfeysdə ağır hesablamanı await Task.Run(() => CountPrimes(4_000_000)) ilə hovuzdakı thread-ə göndər: pəncərə donmur, nəticə isə hazır olanda gəlir. Serverdə (ASP.NET Core) isə Task.Run adətən heç nə qazandırmır — iş sadəcə bir hovuz thread-indən digərinə keçir. Task.Run bir işi fona atmaq üçündür, Parallel isə bir böyük işi hissələrə bölmək üçün.
ConfigureAwait və bloklama tələləri
Qrafik interfeysli tətbiqlərdə (WPF, WinForms, MAUI) sinxronizasiya konteksti var: await-dən sonra kod yenidən UI thread-inə qayıdır ki, düymələri və mətnləri təhlükəsiz dəyişə bilsin. Kitabxana kodu isə UI ilə işləmir, ona görə orada ConfigureAwait(false) yazırlar — «davamı istənilən thread-də icra et». Bu, bir qədər sürətlidir və müəyyən tıxanmaların (deadlock) qarşısını alır. ASP.NET Core-da və konsol tətbiqlərində belə kontekst yoxdur, ona görə tətbiq kodunda ConfigureAwait adətən lazım olmur.
// 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) işlədir// 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();Əsas fikirlər
- I/O işləri üçün
async/await, CPU işləri üçünParallelvə PLINQ. IAsyncEnumerable<T>+await foreachelementləri gəldikcə emal edir; .NET 10-da onlar üçün LINQ var.CancellationTokenzəncir boyu ötürülür; ləğvdəOperationCanceledExceptionatılır, vaxt həddi üçünWaitAsyncvar.- Paralel kodda hər thread öz xanasına yazır və ya
Interlockedişlədir. - «Async sona qədər»:
.Resultvə.Wait()yox; kitabxana kodundaConfigureAwait(false).
Özünü yoxla
10 sual. Hər düzgün cavab XP qazandırır.