1. Giriş
Çoxthreadli tətbiqlərdə race condition olması sual deyil — "nə vaxt" baş verəcək sualındadır. Hətta əgər sən düşünürsən ki, kodun etibarlıdır və yalnız "iki kiçik thread" var və hər şey açıq-aydındır, yarış vəziyyəti ən təhlükəsiz görünən məntiq hissəsində də gizlənə bilər.
Race condition ümumiyyətlə nədir və niyə bu qədər qorxuludur? Təsəvvür et ki, iki nəfər eyni vaxtda eyni vərəqi redaktə etməyə çalışır — biri yazır, o biri sildirir. Bəzən hər şey qaydasındadır, bəzən isə oxunmaz nəticə alınır. Proqramlaşdırmada nəticələr daha maraqlı ola bilər: səhvlər həmişə ortaya çıxmır, yalnız müəyyən, demək olar ki, təsadüfi şərtlərdə.
Race condition (yarış vəziyyəti) — proqramın icra nəticəsinin hansı thread-in resursa və ya əməliyyata əvvəl çatmasından asılı olduğu vəziyyətdir. Bu problem yalnız rəqabətli (çoxthreadli) çıxış zamanı yaranır, yəni iki və ya daha çox thread paylaşılmış məlumatlara və ya resurslara müraciət etdikdə.
Yarış zamanı nə baş verir?
Sadə sxem belədir. Təsəvvür et, bizim iki thread və bir paylaşılmış resursumuz (məsələn, X dəyişəni) var:
+---------+ +---------+
| Thread 1| | Thread 2|
+----+----+ +----+----+
| |
| Oxu X |
| <-------------------|
| |
| Artır X |
|-------------------> |
| |
| Yaz X |
| <-------------------|
Əgər hər iki thread eyni vaxtda X dəyərini oxuyub artırıb yazırlarsa, biri digərinin dəyişikliklərini "üstünə yazacaq" və artımların ümumi sayı gözlənilənə uyğun gəlməyəcək.
2. Race Condition-in klassik nümunəsi
Gəlin nümunəyə baxaq. Fərz edək ki, düymə basmalarının sayını müxtəlif thread-lərdən saymaq və ya işlənmiş tapşırıqların sayını hesablamaq istəyirik.
Sadə bir dəyişən götürürük və onu artıran bir neçə thread:
using System;
using System.Threading;
class Program
{
static int counter = 0; // Paylaşılmış resurs
static void Main()
{
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("Gözlənilən dəyər: 200000");
Console.WriteLine("Faktiki dəyər: " + counter);
}
static void IncrementCounter()
{
for (int i = 0; i < 100_000; i++)
{
counter++; // << Burada problem yarana bilər!
}
}
}
Nə gözləyirik?
Hər thread counter-i 100000 dəfə artırdığı üçün yekun dəyərin 200000 olacağını gözləyirik.
Əslində nə alırıq?
Bəzən — bəli, 200000. Amma çox vaxt dəyər daha kiçik olur — bəzən xeyli daha kiçik. Təcrübəni təkrarlayın, nəticə dəyişəcək!
Niyə belədir?
counter++ əməliyyatı atomik deyil. Əslində o belə həyata keçirilir (sadələşdirilmiş şəkildə):
- Hazırkı counter dəyərini oxu (məsələn, 0)
- Bir vahid artır (alınan 1)
- Geri yaz (counter = 1)
Əgər iki thread eyni köhnə dəyəri oxuyarsa, hər ikisi eyni yeni dəyəri yazaraq praktik olaraq eyni inkrementi tətbiq edə bilər.
İki thread nümunəsində vizualizasiya:
Fərz edək ki, counter = 0.
- Thread 1: 0-ı oxuyur
- Thread 2: 0-ı oxuyur
- Thread 1: 0 + 1 = 1 hesablayır
- Thread 2: 0 + 1 = 1 hesablayır
- Thread 1: 1-i yazır
- Thread 2: 1-i yazır (Thread 1-in inkrementini itirir)
"Təbriklər", bir artımı itirdiniz! Minlərlə və milyonlarla əməliyyat miqyasında nəticə ciddi şəkildə dalğalanır.
3. Daha nümunələr: yalnız inkrement deyil!
Mətbəxdə qarışıqlıq
Daha maraqlı olsun deyə, kiçik bir kafe təsəvvür et. İki aşpaz eyni tava üzərində omlet bişirir, amma hərəkətlərini koordinasiya etmirlər:
- Biri bir omlet qoyur, digər dərhal özünü üstə qoyur — onlar bir-birinə mane olur;
- Biri hesab edir ki, "mən artıq iki omlet qoydum", digər də eyni fikirdədir, amma əslində tavada üçdür, onlar isə dörd olduğunu düşünürlər;
- Xaos başlayır...
Proqramlaşdırmada race condition da eynilə "xaosa" gətirib çıxarır: nəticə sürətli və nəzarətsiz əməliyyatların ardıcıllığından asılıdır.
Thread-lər bir-birinə necə mane olur: məlumatlara eyni vaxtda giriş
Məsələn, bank tətbiqi həyata keçirirsən və müştəri hesabı eyni anda iki thread tərəfindən artırılır və çıxılır (məsələn, biri online-ödəniş, digəri kassada):
account.Balance += 500; // Thread 1: depozit
account.Balance -= 300; // Thread 2: çıxarış
Əgər bu əməliyyatlar qorunmazsa, yekun balans səhv ola bilər: bəzi əməliyyatlar sadəcə "üstünə yazılacaq", əgər thread-lər eyni vaxtda işləyirsə.
4. Faydalı nüanslar
Niyə race condition problemidir?
Tutmaq və reproduksiya etmək çətindir. Səhv yalnız yüklü maşında və ya nadir şəraitdə ortaya çıxa bilər.
Debug etmək çətindir. Debug zamanı thread-lər fərqli davranış göstərə bilər və səhv yoxa çıxa bilər.
Məlumat bütövlüyünün pozulması. Natamam, korlanmış məlumat alırsınız, bəzən bunu ümumiyyətlə görməyə bilərsiz.
Təhlükəsizlik. Kritikal tətbiqlərdə race condition sızmalara, məlumatın məhv olmasına və hətta zəifliklərə səbəb ola bilər.
Yarışın "timing" diaqramı
+-----------------------+ +-----------------------+
| Thread 1 | | Thread 2 |
+-----------------------+ +-----------------------+
| 1. Oxu counter | | |
| 2. Artır counter | | |
| (amma yazma) | | |
| | | 1. Oxu counter |
| | | 2. Artır counter |
| | | 3. Yaz counter |
| | | (counter = 1) |
| 3. Yaz counter | | |
| (counter = 1) | | |
+-----------------------+ +-----------------------+
Hər iki thread inkrement edib, amma yekun — yalnız bir inkrement yazılıb!
Race condition harada tez-tez rast gəlinir
- Bir neçə thread-in müraciət etdiyi qlobal və ya static dəyişənlər.
- Müxtəlif thread-lərdən doldurulan siyahılar, növbələr, kolleksiyalar.
- Event və delegate-lər, əgər subscribe/unsubscribe eyni vaxtda baş verirsə (məsələn, UI + background task-lar).
- Keşləmə, dictionary-lər, bağlantıların idarəsi.
- Fayllarla, loglarla, verilənlər bazası ilə hər hansı qarşılıqlı əlaqə tranzaksiya və ya bloklaşdırma olmadan.
Race condition-dən necə qaçmaq: qısa giriş
- Sinxronizasiya! (ətraflı — sonrakı mühazirələrdə).
- Dil və kitabxana konstruksiyalarından istifadə et: lock, Monitor, mutex-lər, semaforlar və s.
- Sadə əməliyyatlar üçün atomik metodlardan istifadə et (Interlocked.Increment və s.).
- Thread-safe kolleksiyalardan istifadə et (ConcurrentBag, ConcurrentDictionary).
- Həmişə düşün: "əgər iki funksiya eyni zamanda çağırılsa, nə olacaq?"
5. Faydalı məsləhətlər
Yarışların axtarışı və diaqnostikası üçün məsləhətlər
- Bir neçə thread istifadə edirsinizsə, hətta ən sadə əməliyyatlara (inkrement ++, təyinat) inanmıayın.
- Mümkün olduqda dəyişənlərin paylaşımından çəkinin.
- "Dalğalanan" bug-lar, çətin reproduksiya olunan səhvlər görsəniz — yarışları düşünün!
- Thread analizi alətlərindən istifadə et (dotTrace, Concurrency Visualizer, Thread Sanitizer).
- Yük testləri aparın — thread və əməliyyat sayı artdıqca səhv tapmaq ehtimalı artır.
Sinxronizasiya olmadan nə edilə və nə edilməməlidir
| Əməliyyat | Çoxthread mühitində təhlükəsiz? | Şərh |
|---|---|---|
| Təyinat int | 🟩 Bəzən* | Yalnız əgər bir thread yazır, digərləri oxuyursa; yoxsa — yarış |
| İnkremet (++/--) | 🟥 Xeyr | Atomik deyil! Race Condition |
| Oxu string | 🟩 Bəzən* | Əgər string yaradıldıqdan sonra dəyişmirsə |
| Obyektə təyinat | 🟩 Bəzən* | Əgər eyni vaxtda yazılma yoxdursa |
| List<T>-ə əlavə etmək | 🟥 Xeyr | List<T> thread-safe deyil |
|
🟩 Bəli | Xüsusi atomik metod |
— "Bəzən" o deməkdir ki, əgər yalnız bir thread yazır və digərları yalnız oxuyursa, bu təhlükəsiz ola bilər; əgər bir neçə thread eyni vaxtda yazırsa — həmişə yarış var.
6. Tipik səhvlər və tələlər
Yuxarıdakı demo-kodda counter++-in problem olduğunu gördük. Başqa bir tələyə misal: şərtdə dəyəri artırmaq və ya yoxlamaq.
Nümunə: "ilk işə başlama" ilə maraqlı səhv
if (!alreadyStarted)
{
alreadyStarted = true;
// İnisializasiya edirik...
}
Əgər bu şərti bir neçə thread eyni vaxtda yerinə yetirirsə, hər biri alreadyStarted == false görüb içəri daxil ola bilər! Nəticə — bir şeyi iki dəfə inisializasiya etmək, bu da səhvə gətirib çıxara bilər.
GO TO FULL VERSION