CodeGym /Kurslar /C# SELF /Ümumi resursların problemi

Ümumi resursların problemi

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

1. Giriş

Çoxiplikli tətbiqdə ümumi resurs — iki və ya daha çox thread-in eyni vaxtda daxil ola bildiyi hər şeydir. Bu ola bilər:

  • Dəyişən (məsələn, qlobal sayğac və ya siyahı).
  • Obyekt (məsələn, istifadəçilərin kolleksiyası).
  • Fayl və ya şəbəkə socket-i.
  • Müxtəlif thread-lər tərəfindən dəyişdirilən hər hansı bir data strukturu.

Konsol tətbiqimizdə ən çox dəyişənlərə və obyektlərə rast gələcəyik ki, onlar thread-lər arasında "paylaşılıb".

Analoji

Fərz edin iki adam eyni dəftərə eyni vaxtda nəsə yazmağa çalışır və növbə ilə razılaşmayıblar. Ən yaxşı halda — çirkli yazı alınacaq, pis halda — kiminsə məlumatı üstündən yazılacaq. Proqramlaşdırmada vəziyyət tamamilə eynidir, sadəcə bu "adamlar" thread-lərdir.

Məlumat yarışına məruz qalan tipik resurslar qısaca

Aşağıdakı cədvəldə — müxtəlif thread-lərin eyni vaxtda daxil olması üçün təhlükəli olan ən çox rast gəlinən resurslar:

Resurs Məsələ qrupları Nümunə
Dəyişənlər tipi int Səhv artım/azalma Sayıcılar, indekslər
Ümumi kolleksiyalar Elementlərin itməsi/zədələnməsi, exceptions Ümumi sifariş siyahısı
Obyektlər Vəziyyətin ardıcıl olmaması Flag-lər, property-lər
Fayllar Data-nın pozulması, səhv oxu/yazı Log-fayllar, konfiqurasiya

2. Race condition: necə özünü göstərir?

Nümunə: Ziyarətçi sayğacı

Tutaq ki, istifadəçinin neçə dəfə düyməni basdığını saymaq istəyirik (və ya bizim nümunədə müxtəlif thread-lərin dəyişəni neçə dəfə inkrement etdiyini). Sadə kod versiyası:


int counter = 0;

void Increment() {
    counter++;
}

İndi iki thread yaradaq, hər birində Increment() 100 000 dəfə çağırılır:


using System;
using System.Threading;

class Program
{
    static int counter = 0;

    static void Increment()
    {
        for (int i = 0; i < 100_000; i++)
        {
            counter++;
        }
    }

    static void Main()
    {
        Thread t1 = new Thread(Increment);
        Thread t2 = new Thread(Increment);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"Gözləyirdik: 200000, aldıq: {counter}");
    }
}

Loqik olaraq counter neçə dəfə artmalıdır? 200000! Amma bu kodu bir neçə dəfə işə salsanız, praktiki olaraq fərqli rəqəmlər görəcəksiniz: 185000, 192500, 198765… Niyə?

3. Niyə counter++ atomik əməliyyat deyil?

counter++ necə əslində işləyir

C# və digər yüksək səviyyəli dillərdə proqram maşın təlimatlarına çevrilir. Təəssüf ki, counter++ operatoru tək bir sehrli "1 əlavə et" əmrinə çevrilmir. Əslində baş verənlər:

  1. Thread yaddaşdan dəyəri OXUYUR (counter).
  2. Bu dəyəri 1-ə artırır (prosessor registerində).
  3. Yeni dəyəri yaddaşa YAZIR (counter).

Əgər iki thread eyni vaxtda bunları edirsə, ikisi də köhnə dəyəri oxuya, artırıb, və hər ikisi eyni nəticəni yaddaşa yazaraq bir inkrementi itirə bilər.

Race ssenarisi

Tutaq counter 1000-ə bərabər idi. Hər iki thread bu dəyəri oxudu (addım 1), hər ikisi onu 1001-ə çatdırdı (addım 2), və sonra hər ikisi 1001-i yaddaşa yazdı (addım 3). Nəticə: bir inkrement itdi!

Race-in vizuallaşdırılması

Zaman anı Thread 1 Thread 2 counter dəyəri
1 1000-ni oxudu 1000
2 1000-ni oxudu 1000
3 1001-ə artırdı 1001-ə artırdı 1000 (hələ yazılmayıb)
4 1001 yazdı 1001
5 1001 yazdı 1001

Beləcə iki inkrement əvəzinə yalnız bir artım baş verdi!

4. Bir neçə əlavə nümunə: "görünməyən səhvlər"

Race condition rəqəmlərlə olmayanda nə olar?

İndi bir neçə thread-in eyni siyahıya element əlavə etdiyini təsəvvür edək:


using System;
using System.Collections.Generic;
using System.Threading;

class Program
{
    static List<int> numbers = new List<int>();

    static void AddNumbers()
    {
        for (int i = 0; i < 10000; i++)
        {
            numbers.Add(i);
        }
    }

    static void Main()
    {
        Thread t1 = new Thread(AddNumbers);
        Thread t2 = new Thread(AddNumbers);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"Gözləyirdik: 20000, aldıq: {numbers.Count}");
    }
}

Bu kod da hər işə salmada fərqli nəticə verə bilər: bəzən proqram exception ilə çökə bilər, bəzən gözlənildiyindən az element görə bilərsiniz.

Niyə? Çünki standart List<T> thread-safe deyil. Yəni iki thread eyni vaxtda Add çağıranda siyahının daxili strukturu zədələnə bilər.

5. Atomiklik əməliyyatları

Atomik əməliyyat nədir?

Atomik əməliyyat aralıqda başqa bir thread tərəfindən kəsilə bilməyən, tamamilə icra olunan əməliyyatdır. Bu bir növ "transaction" kimidir: ya hamısı, ya heç bir şey.

  • int tipli təyinatlar məsələn myVar = 42; əksər platformalarda atomikdir (əgər böyük obyekt deyilsə).
  • Amma counter++ atomik deyil — bu üç ardıcıl addımdan ibarətdir.

Xüsusi atomik əməliyyatlar

.NET-də atomik əməliyyatlar üçün xüsusi siniflər var: məsələn, Interlocked. Bu yanaşmanı növbəti mühazirələrdə ətraflı görəcəyik.

Interlocked.Increment vasitəsilə atomik inkrement nümunəsi:


using System.Threading;

int counter = 0;
Interlocked.Increment(ref counter); // atomik əməliyyat!

6. Niyə race condition tutmaq çətindir?

Race condition təhlükəlidir, çünki:

  • Yüksək yüklənmə altında özünü göstərə bilər.
  • 100% deyil, 5% və ya hətta 0.01% hallarda tuta bilərsiniz.
  • "Random" şəkildə çökə bilər və gözlənilməz yerlərdə peyda olar.

Məsələnin necə tanınması?

Proqramı hər işə saldıqda fərqli (və səhv) nəticələr alırsınızsa, məlumat yarışını şübhələnin.

Proqramçılar zarafatı

"Əgər səhv nadir hallarda çıxır və Thread.Sleep(50) əlavə etməklə düzəlirsə — sizin problemləriniz göründüyündən də ciddi-dir."

7. Faydalı nüanslar

Sinxronizasiya

Kritik bölmələri (ümumi resurslarla işləyən kod hissələri) qorumaq üçün onları sinxronizasiya etmək lazımdır. Amma bu növbəti mühazirələrin mövzusudur. İndilik əsas odur ki, problemi görməyi və izah etməyi öyrənəsiniz.

Yeni başlayanların tipik səhvləri

Çox başlanğıc proqramçılar düşünür: “Mənim counter++ — nə zərəri ola bilər?” Təəssüf ki, bir dəfə sistemdə iki və ya daha çox thread yarananda, hər şey səhv ola bilər! Hətta görünüşdə sadə əməliyyatlar: dəyişənlərin oxunması/yazılması, siyahıya element əlavə etmək, obyektin vəziyyətini dəyişmək və s.

Real dünyada məlumat yarışlarının rolu

Müasir çoxiplikli tətbiqlərdə (məsələn, server API-lərində, veb sorğuların işlənməsində, oyunlarda və mobil tətbiqlərdə) ümumi resurslar demək olar ki, həmişə olur. Sinxronizasiya olmadan race condition sifarişlərin səhv işlənməsinə, çöküşlərə, yaddaş sızıntılarına və düzəltməsi çox çətin olan səhvlərə gətirib çıxarır.

Middle/senior səviyyəli intervyularda sizdən mütləq soruşacaqlar: "Race condition nədir? Onu necə önləmək olar?" Əgər yuxarıdakı nümunələri gətirə və mexanikanı izah edə bilsəniz — recruiter-lər məmnun qalacaq!

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