Məzmuna keç
Educora
İrəli23 dəq14 / 16

Asinxron və paralel proqramlaşdırma

Gözləmə tələb edən və hesablama tələb edən işləri ayır: asinxron axınlar, ləğvetmə token-ləri, `Parallel` və PLINQ, `ConfigureAwait` və bloklama tələləri.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • I/O və CPU işləri üçün düzgün aləti seçmək
  • IAsyncEnumerable<T> yazmaq və await foreach ilə oxumaq, işi CancellationToken ilə dayandırmaq
  • Hesablamaları Parallel və 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, SQLasync/await, Task.WhenAll
Hissə-hissə gələn verilənlərsəhifələnmiş API, böyük faylIAsyncEnumerable<T>, await foreach
CPU: hesablamasadə ədədlər, hesabatlar, şəkillərParallel.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.

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;
    }
}
Gözlənilən nəticə
Received 95
Received 78
Received 88
80 and above: 95, 88
Hər bal gələn kimi çap olunur; ikinci hissədə asinxron axına LINQ tətbiq edilib (.NET 10)

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.

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");
    }
}
Gözlənilən nəticə
Part 1 downloaded
Cancelled: it took too long
450 ms-də ləğv: birinci hissə (300 ms) yükləndi, ikincisi yarımçıq qaldı

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.

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}";
}
Gözlənilən nəticə
Timeout: showing yesterday's cached results
Server 2 saniyə çəkir, biz isə yalnız 0,5 saniyə gözləyirik

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.

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;
}
Gözlənilən nəticə
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örd hesablama paralel gedir; nəticələr isə həmişə eyni sıra ilə çap olunur, çünki hər biri öz xanasındadır
Yarış vəziyyəti
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())

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.

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);
}
Kitabxana metodu: token qəbul edir və ConfigureAwait(false) işlədir
Səhv: `.Result` ilə bloklama
// blocks the thread while waiting;
// in a UI app (WPF, WinForms) this can freeze or deadlock
string report = LoadReportAsync().Result;
Düzgün: `await`
// 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 üçün Parallel və PLINQ.
  • IAsyncEnumerable<T> + await foreach elementləri gəldikcə emal edir; .NET 10-da onlar üçün LINQ var.
  • CancellationToken zəncir boyu ötürülür; ləğvdə OperationCanceledException atılır, vaxt həddi üçün WaitAsync var.
  • Paralel kodda hər thread öz xanasına yazır və ya Interlocked işlədir.
  • «Async sona qədər»: .Result və .Wait() yox; kitabxana kodunda ConfigureAwait(false).

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
Bütün nüvələrdə sadə ədədləri saymaq üçün hansı alət uyğundur?