1. Giriş
Bu gün biz böyük həcmli məlumatları mümkün qədər sürətlə necə işlətməyi öyrənəcəyik, bütün mövcud prosessor nüvələrindən istifadə etməklə (sizin kompüterinizdə və ya serverdə). Bunun üçün bizə System.Threading.Tasks.Parallel namespace-indən siniflər lazım olacaq, əsasən Parallel.For və Parallel.ForEach metodları.
Əgər tapşırıq — tamamilə CPU-bound-disə nə etmək lazımdır?
Klassik for və ya foreach dövrü elementləri ardıcıl şəkildə emal edir. Sadə və etibarlı. Amma əgər sizdə çoxnüvəli prosessor varsa, dövr yalnız bir nüvəni istifadə edir və digər nüvələr boş qalır. Niyə iş parçalarını fərqli nüvələrə paylamayaq ki, eyni anda emal olunsun?
Nümunə:
// 1-dən N-ə kvadratların cəmini hesab edirik
long sum = 0;
for (int i = 1; i <= 1_000_000; i++)
{
sum += i * i;
}
Bu kod sadədir, amma ardıcıl işləyir. Bəs tapşırıqları nüvələrə yaysaq?
Tanış olun: Parallel.For və Parallel.ForEach
Bu nədir?
- Parallel.For — adi for dövrü kimi işləyir, amma işi hissələrə bölür və avtomatik olaraq thread-lərə paylayır, mövcud nüvələrdən istifadə edir.
- Parallel.ForEach — kolleksiyanı adi foreach kimi emal edir, amma paralel şəkildə.
Rəsmi sənədləşmə:
Niyə bu rahatdır?
Siz özünüz thread-ləri yaratmağa, işə salmağa və idarə etməyə məcbur deyilsiniz. Framework bütün ağır işi sizin üçün edir. Siz adi dövrə oxşayan kod yazırsınız, paralelliyi isə "kapotun altında" framework təmin edir.
2. Sintaksis: əsas nümunələr
Parallel.For
long total = 0;
Parallel.For(1, 1_000_001, i =>
{
// Bu lambda fərqli thread-lər tərəfindən eyni anda icra oluna bilər
Interlocked.Add(ref total, i * i); // Race şərtlərinin qarşısını almaq üçün
});
Console.WriteLine($"Kvadratların cəmi: {total}");
Diqqət: total dəyişəni biz Interlocked.Add vasitəsilə yeniləyirik — data race-lərin qarşısını almaq üçün.
Parallel.ForEach
var numbers = Enumerable.Range(1, 10_000_000).ToArray();
long sum = 0;
Parallel.ForEach(numbers, num =>
{
Interlocked.Add(ref sum, num * num); // Təhlükəsiz toplama
});
Console.WriteLine($"Kvadratların cəmi: {sum}");
Daxildən baxış (Vizual sxem)
+-------------------+
|Kolleksiya/interval |
+---------+---------+
|
v
+----------------------+
| Parallel.ForEach |
+----------+-----------+
|
+----+----+----+----+
| | |
v v v
Task #1 Task #2 Task #3 ... (mövcud nüvələr)
| | |
+--+----+ +--+-----+ +--+-----+
|Emal | |Emal | |Emal |
+-------+ +--------+ +--------+
\ | /
+--------+--------+
|
v
Nəticə
3. Böyük faylların analizi (CPU-bound işləmə)
Tutaq ki, bizim on minlərlə sətrdən ibarət mətn faylımız var — məsələn, hər sətrdə bir ədəd var. Faylı oxuyub hər ədədi kvadrata qaldırmaq və kvadratların cəmini hesablamaq lazımdır.
Sinxron versiya
string[] lines = File.ReadAllLines("numbers.txt");
long sum = 0;
foreach (var line in lines)
{
if (long.TryParse(line, out long n))
{
sum += n * n;
}
}
Console.WriteLine($"Kvadratların cəmi: {sum}");
Parallel.For ilə paralel versiya
string[] lines = File.ReadAllLines("numbers.txt");
long sum = 0;
Parallel.For(0, lines.Length, i =>
{
if (long.TryParse(lines[i], out long n))
{
Interlocked.Add(ref sum, n * n);
}
});
Console.WriteLine($"Kvadratların cəmi: {sum}");
Nə dəyişdi: biz adi dövrü paralel dövrlə əvəz etdik və sum indi Interlocked.Add ilə artırılır — threadlər arasında konfliktlərin qarşısını almaq üçün.
4. Kapotun altında nə baş verir?
Siz Parallel.For və ya Parallel.ForEach çağıranda, .NET işi avtomatik olaraq parçalayır və mövcud prosessor nüvələrinə paylayır, thread pool-dan istifadə edir. Hər bir parça öz thread-i üzərində müstəqil emal olunur.
Üstünlük: əgər sizdə 4 nüvə varsa, iş təxminən 4 dəfə sürətlənə bilər (əgər tapşırıq xarici resurslardan asılı deyil və yaddaş və ya disk oxuma sürəti kimi məhdudiyyətlərə vurulmur).
İcra müddətini müqayisə edək
var numbers = Enumerable.Range(1, 100_000_000).ToArray();
long sumSync = 0;
var sw = System.Diagnostics.Stopwatch.StartNew();
foreach (var n in numbers)
sumSync += n * n;
sw.Stop();
Console.WriteLine($"Sync: {sw.ElapsedMilliseconds} ms, cəm: {sumSync}");
long sumParallel = 0;
sw.Restart();
Parallel.ForEach(numbers, n =>
Interlocked.Add(ref sumParallel, n * n)
);
sw.Stop();
Console.WriteLine($"Parallel: {sw.ElapsedMilliseconds} ms, cəm: {sumParallel}");
Sınaq et! Güclü maşında sürətlənmə əhəmiyyətli ola bilər, amma nəticə tapşırıqdan və tıxaclardan asılıdır.
5. Faydalı nüanslar
Paralelliyin dərəcəsini idarə etmək
Bəzən istifadə olunan thread sayını məhdudlaşdırmaq məntiqli olur (məsələn, sistemi yükləməmək üçün). Bunun üçün MaxDegreeOfParallelism-dan istifadə edin:
using System.Threading.Tasks;
long sum = 0;
var options = new ParallelOptions {
MaxDegreeOfParallelism = 2
};
Parallel.For(0, 100, options, i =>
{
Interlocked.Add(ref sum, i * i);
});
Console.WriteLine($"Kvadratların cəmi: {sum}");
Harada faydalıdır: əgər bəzi hesablamalar diskə daha çox yüklənirsə, yoxsa CPU-ya yox — thread sayını azaldıb performansın necə dəyişdiyini yoxlamaq faydalıdır.
Paralel dövrlər nə zaman istifadə edilməlidir
| Adi for | Parallel.For/Parallel.ForEach | |
|---|---|---|
| Prosessorlar | Bir nüvəni istifadə edir | Bütün nüvələri istifadə edir |
| Sıra | Zəmanətlidir | Zəmanət verilmir |
| Sürət | Adətən daha yavaşdır | Çox vaxt əhəmiyyətli dərəcədə daha sürətli |
| Sadəlik | Çox sadədir | Thread-safety nəzərə almağı tələb edir |
| Ən uyğun tətbiq | Kiçik həcmli məlumatlar, I/O-bound | Böyük həcmli məlumatlar, CPU-bound |
Genişləndirmə: Parallel nələr edə bilir?
Parallel.Invoke() — bir neçə müstəqil metodu eyni zamanda işə salır:
static void DoTask1() => Console.WriteLine("Vəzifə 1 yerinə yetirildi");
static void DoTask2() => Console.WriteLine("Vəzifə 2 yerinə yetirildi");
static void DoTask3() => Console.WriteLine("Vəzifə 3 yerinə yetirildi");
Parallel.Invoke(
() => DoTask1(),
() => DoTask2(),
() => DoTask3()
);
Hər bir metod mümkün olduqda öz nüvəsində işlənəcək.
Real həyatda tətbiqlər
- Şəkil emalı: müxtəlif blokların eyni anda emalı (məsələn, filtr tətbiqi).
- Müstəqil massiv hesablamaları: maliyyə hesablamaları, modelləşdirmə (portfel qiymətləndirilməsi ssenarilərə görə).
- Böyük log faylları ilə işləmək: axtarış və agregasiya bir neçə nüvədə.
- Machine learning: müstəqil tapşırıqlara bölünən işlər (data batch-ləri, feature engineering).
Və əlbəttə, müsahibədə siz paralel dövrlərin nə olduğunu danışmaqla yanaşı, onların üstünlüklərini və çatışmazlıqlarını da dürüstcə izah edə biləcəksiniz.
6. Parallel.For və Parallel.ForEach ilə işləməkdə tipik səhvlər
Səhv №1: Data race-ləri nəzərə almamaq.
Ümumi dəyişəni Interlocked və ya lock istifadə etmədən yeniləmək nəticələrin səhv olmasına səbəb olur, çünki thread-lər eyni anda həmin yaddaşa daxil ola bilər.
Səhv №2: I/O-bound tapşırıqlar üçün istifadə etmək.
Paralel dövrlər diskə və ya şəbəkəyə bağlı tapşırıqları sürətləndirmir, hətta overhead səbəbindən onları yavaşlada bilər.
Səhv №3: İcranın ardıcıllığına güvənmək.
Paralel dövrlər elementlərin işlənmə ardıcıllığını zəmanət vermir, əgər məntiq ardıcıllığa bağlıdırsa, bu pozula bilər.
Səhv №4: Yan təsirləri nəzərə almamaq.
Paralel dövrlər zamanı ümumi vəziyyətin (məsələn, kolleksiyaların) dəyişdirilməsi səhvlərə yol aça bilər, əgər thread-safe strukturlar və ya sinxronizasiya mexanizmləri istifadə olunmazsa.
GO TO FULL VERSION