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ı 100 və 200 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 Balance-ı 0 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:
- Yaddaşdan oxu: register = counter;
- Artır: register = register + 1;
- 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 counter-ı 0 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 | |
| Bir neçə thread yazır | Bəli | |
| 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.
GO TO FULL VERSION