CodeGym /Kurslar /C# SELF /Verilərin paralel işlənməsi

Verilərin paralel işlənməsi

C# SELF
Səviyyə , Dərs
Mövcuddur

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.ForParallel.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.ForParallel.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.ForParallel.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.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION