CodeGym /Kurslar /C# SELF /Race Conditions — dərindən təhlil

Race Conditions — dərindən təhlil

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

1. Daha mürəkkəb Race Condition nümunələri

Race Condition — ən hiyləgər səhvlərdən biridir. Niyə? Çünki onlar adətən nadir baş verir, prosessor sürətindən, OS gecikmələrindən, thread-lər arasında təsadüfi kontekst switch-lərindən asılıdır və adətən siz evə getməli olduğunuz vaxtda ortaya çıxırlar. Heç bir static analyzer onları tutmur. Unit-testlər də (əgər siz clairvoyant deyilsinizsə) tutmur, amma bir dəfə... prodakşndə özünü göstərirlər.

Belə gözlənilməz sürprizlərin qarşısını almaq üçün race condition-ların necə yarandığını və onlarla nə etmək lazım olduğunu yaxşı başa düşmək lazımdır.

Nümunə 1. Bank, amma bank etikası yoxdur

Tutaq ki, çox sadə bir bank hesabı klassımız var:

public class Account
{
    public int Balance = 0;

    public void Deposit(int amount)
    {
        Balance += amount;
    }

    public void Withdraw(int amount)
    {
        Balance -= amount;
    }
}

Təsəvvür edin ki, iki thread eyni anda hesabı 100200 avro ilə doldurmağa çalışır, sonra hər biri növbə ilə 50 avro çıxarır. Təkthread dünyasında balans həmişə belə olar: (0 + 100 + 200 - 50 - 50) = 200.

Amma multithread reallığında nə baş verir? Gəlin eksperiment edək:

var acc = new Account();

var t1 = new Thread(() => {
    acc.Deposit(100);
    acc.Withdraw(50);
});
var t2 = new Thread(() => {
    acc.Deposit(200);
    acc.Withdraw(50);
});

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

Console.WriteLine(acc.Balance); // Hər run fərqli cavab verə bilər!

İzahedici:
Əgər hər iki thread Balance0 kimi oxuyub, sonra biri 100, digəri isə 200 yazsa, amma bu əməliyyatlar arasında kontekst switch baş versə — balans “pul itirə” və ya “əlavə pul qazana” bilər. Lock-lar olmayan bank — fırıldaqçının arzusudur!

Nümunə 2. Flag dəyişəni

Çox yayılmış səhv — vəziyyəti göstərmək üçün flag dəyişənindən istifadə etmək:

bool isReady = false;

void Worker()
{
    while (!isReady)
    {
        // gözləyirik...
    }
    // nəsə edirik
}

Başqa bir thread isReady = true yazmış ola bilər. İlk baxışdan təhlükəsiz görünür: nə yanlış ola bilər? Çıxır ki, hətta bool dəyişəni oxumaq və yazmaq da təhlükəsiz olmaya bilər! Səbəb: compiler optimizations, CPU cache, instruction reordering çoxnüvəli sistemlərdə.

Nə baş verə bilər?
Bir thread dəyişiklikləri görə bilməyib sonsuz dövrdə ilişib qala bilər, digər thread artıq isReady = true qoysa belə. Thread-lər arasında “flag” ötürmək üçün həmişə xüsusi primitvlərdən istifadə edin (volatile, Interlocked, event-lər və s. — ətraflı üçün bax rəsmi sənədlərə).

Nümunə 3. null yoxlanışı və obyekt yaradılması

Singleton klassını təsəvvür edin:

public class Singleton
{
    private static Singleton _instance;

    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new Singleton();
            }
            return _instance;
        }
    }
}

Kod sadə görünür? Amma əgər çoxlu thread-lər eyni anda Instance-ə müraciət etsə, eyni anda bir neçə instance yarana bilər! Bu double-check yoxlamasının sinxronizasiyasız klassik nümunəsidir: sahə _instance yarışla inicializasiya oluna bilər, əgər lock ilə qorunmazsa.

2. Race Condition kodda necə yaranır

“Read-modify-write” əməliyyatı — şərin anatomiyası

İnkrement nümunəmizə qayıdaq: counter++

CPU terminlərində bu belədir:

  1. Yaddaşdan oxu: register = counter;
  2. Artır: register = register + 1;
  3. Geri yaz: counter = register;

Əgər iki thread bu bloku demək olar ki, eyni vaxtda icra edərsə nə olar?

  • Hər iki thread counter0 kimi oxuyur.
  • Hər iki thread internal register-i 1-ə artırır.
  • Hər ikisi geri 1 yazır.

Nəticə: iki inkrement əvəzinə dəyər cəmi yalnız 1-ə artdı! Bir artım “yeyildi” — və heç kim fərq etmədi.

İki thread-in “toqquşmasının” vizuallaşdırılması


Thread A        |   Thread B        |   counter-------------------------------------------
Oxuyur (0)      |                   |   0
                |   Oxuyur (0)      |   0
İnkrem (1)      |                   |   0
                |   İnkrem (1)      |   0
Yazır (1)       |                   |   1
                |   Yazır (1)       |   1  <-- Oops! Gözləyirdik 2, gəldi 1

Race Condition-i niyə tutmaq çətindir?

  • Race Condition “maskalanır”. Koda bir-iki Console.WriteLine əlavə etsin — səhv yox ola bilər. Sadəcə thread-lər başqa yol tutdu.
  • Səhv nüvələrin sayından, onların yüklənməsindən, OS və .NET versiyasından asılıdır. Dünən işləyirdi, bu gün dağıldı.
  • Nəticə dəyişkəndir: səhv həmişə görünmür, yalnız “xüsusi” hallarda.
  • Debug etmək əlavə əyləncədir.

3. Koleksiyalarda və .NET klasslarında Race Condition

Race Condition yalnız dəyişənlərlə olmur. Collections, queue-lar və hətta standart .NET klassları hər zaman qorunmalı deyil.

Nümunə: List<T> thread-safe deyil

Əgər iki thread eyni zamanda adi List<T>-də .Add() edərsə, nəticə fəlakətli ola bilər:

var list = new List<int>();
var tasks = new List<Task>();

for (int t = 0; t < 10; t++)
{
    int threadNum = t;
    tasks.Add(Task.Run(() => {
        for (int i = 0; i < 1000; i++)
            list.Add(threadNum * 1000 + i);
    }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(list.Count); // Çox güman ki < 10000 olacaq

Burada Race Condition Add() daxilində yaranır — collection daxili array-ı genişləndirə, elementləri kopyalaya bilər və bu vaxt başqa thread artıq əlavə edir... Nəticə — itmiş data, istisnalar və ya pozulmuş collection.

Nümunə: Bir neçə thread eyni indexer-i istifadə edir

var array = new int[10];
void Worker(int index, int value)
{
    array[index] = value;
}

Parallel.Invoke(
    () => Worker(5, 1),
    () => Worker(5, 2)
);

Nəticədə array[5]-də ya 1, ya da 2 olacaq — kimin son yazdığına görə. Bəzən belə davranış istənilən olur, amma daha çox hallarda bu incə və çətin tutulan səhvlərin mənbəyidir.

4. Faydalı nüanslar

Race Condition və non-atomic əməliyyatlar

Əgər əməliyyat “kiçik” görünürsə də, atomic olmadığı halda diqqətli olun.

  • i++, i--
  • a = b
  • myObject.Property = value
  • “Flag-a baxdı — sonra dəyəri dəyişdirdi”
  • “Obyekt götürdü — sonra onun daxili vəziyyətini dəyişdirdi”

Bunların hər biri ayrıca atomic əməliyyat deyil! Mikroaddımlar arasında başqa thread-lər onları “ələ keçirə” bilər.

Race Condition barədə miflər və tələlər

Mif 1: “Əgər dəyişən int-disə, onunla hər şey təhlükəsizdir, primitive-dir”.
Reallıq: Hətta int üzərində əməliyyatlar atomicliyi təmin etmir, əgər onlar volatile yoxdursa və xüsusi API-lə istifadə olunmursa.

Mif 2: “Bir thread yazır, digəri yalnız oxuyur — hər şey qaydasındadır”.
Reallıq: Yox! .NET kolleksiyaları eyni zamanda oxuma və yazma zamanı bütövlük təmin etmir, “sındırılmış” obyektlər və ya qəribə istisnalar ala bilərsiniz.

Mif 3: “Race Condition yalnız super-yüklü servislərdə çıxır”.
Reallıq: Kiçik utilitlərdə, oyunlarda, desktop proqramlarda da rəqabət vəziyyətinə düşə bilərsiniz. Sadəcə daha nadir və ya gec görünür.

Race Condition-dan necə qorunmaq

  • Sinxronizasiya primitvlərindən istifadə edin (lock, Mutex, Monitor və s.).
  • Riyazi əməliyyatlar üçün Interlocked-dən istifadə edin (sənəd).
  • Collections üçün spesifik thread-safe siniflərdən istifadə edin (ConcurrentBag, ConcurrentDictionary və s. — bax sənədlər).
  • Flag və vəziyyət dəyişənlərini qorunmasız istifadə etməyin (volatile, event-lər, sinxronizasiya primitvləri).

Race Condition nə vaxt yaranır

Ssenari Race Condition ola bilər? Necə qorunmaq
Bir neçə thread məlumatı oxuyur Yox (əgər dəyişiklik yoxdursa) -
Bir thread yazır, digər thread oxuyur Bəli
lock/volatile
Bir neçə thread yazır Bəli
lock/Interlocked
Bir neçə thread kolleksiyanı dəyişdirir Bəli Thread-safe kolleksiyalar
Bir neçə thread flag istifadə edir Bəli volatile, lock, event-lər

Atomiklik — hər şeyi həll etmir

Bəzən atomik əməliyyatlar belə məntiqi “yarış”lardan xilas etmir.

Nümunə:

if (!cache.ContainsKey(key))
{
    cache[key] = GetData(key);
}

Əgər iki thread eyni anda bu if-ə girsə, hər ikisi açarı yoxlayıb yoxdur görəcək və hər ikisi yeni dəyər yaradacaq — ikinci biri birincinin nəticəsini üzərindən yazacaq! Burada int üzərində atomik əməliyyatlar və ya Interlocked kömək etməz — tam bloklama və ya xüsusi funksiya lazımdır (GetOrAdd ConcurrentDictionary-də).

4. Tələbələrin Race Condition ilə bağlı tipik səhvləri

Səhv №1: düşünürlər ki, lock yalnız “böyük şeylər” üçün lazımdır.
Amalda hətta sadə dəyişən qorunmasız qalsın dağıla bilər.

Səhv №2: yanlış obyekti lock edirlər.
Məsələn, string və ya sinifdən kənarda əlçatan obyekt lock etmək. Nəticədə sinxronizasiya gözlənildiyi kimi işləmir.

Səhv №3: “nəticə demək olar ki, həmişə düzgündür” prinsipi ilə işləyirlər.
Əgər bug nadir görünürsə — bu onu qorunmaya ehtiyac olmadığını göstərməz.

Səhv №4: async metodlardan sinxronizasiya olmadan istifadə edirlər.
“Bəlkə də” ümidinə qalıb, ən qeyri-müntəzəm yerlərdə xaotik səhvlə üzləşirlər.

Səhv №5: double-check ilə Singleton yazırlar, amma lock etmirlər.
Bir yaddaşda bir deyil, bir neçə obyekt yaranır və debuq etmək kabusa çevrilir.

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