1. Klassik Coffman şərtləri
1971-ci ildə kompüter alim Edward G. Coffman Jr. dörd şərt təsvir etdi ki, bunlar olmadan deadlock mümkün deyil. Bu qaydalar çoxdan multi-threading müsahibələrinin və kurslarının " əlifbasına " çevrilib. Onları əzbər bilmək kifayət deyil — nə üçün lazım olduqlarını başa düşmək də vacibdir:
- Mutual Exclusion (Öz arası istisna): ən azı bir resurs eyni anda yalnız bir thread tərəfindən əldə edilə bilər.
- Hold and Wait (Saxlama və gözləmə): bir resursu əldə etmiş thread digər resursları gözləyə bilər.
- No Preemption (Məcburi alınmama): resurs zorla thread-dən alınmır — yalnız həmin thread onu azad edə bilər.
- Circular Wait (Dairəvi gözləmə): hər birinin növbəti tərəfindən saxlanılan bir resursu gözləyən thread zənciri mövcuddur.
Əgər bu dörd şərt hamısı yerinə yetirilirsə, deadlock mümkündür. Ən azı birini pozsanız — qarşılıqlı bloklama riski aradan qalxır.
Tez qərar cəhdləri
flowchart TD
A[Bir neçə resursu bloklamaq lazımdır?] -- Xeyr --> B[Standard lock]
A -- Bəli --> C[Həmişə eyni sırada ala bilirsiniz?]
C -- Bəli --> D[Həmişə eyni sırada alın]
C -- Xeyr --> E[Nested lock-lardan qaçın]
D --> F[Lock altında vaxtı minimallaşdırın]
E --> F
B --> F
F --> G{Hələ də qorxulu görünür?}
G -- Bəli --> H[Timeout və retry əlavə edin]
G -- Xeyr --> I[Rahat yatın!]
2. Deadlock-un qarşısını almaq üçün əsas strategiyalar
Deadlock çoxthreadli dünyanın bir parçasıdır, bizim işimiz onunla necə yaşamağı öyrənməkdir. Sadədən mürəkkəbə müxtəlif strategiyalar kömək edir.
Həmişə resursları eyni sırada bloklayın
Məntiq sadədir: əgər bütün thread-lər resursları həmişə eyni sırada əldə edirlərsə, dairəvi gözləmə yaranmayacaq. Bu yanaşmaya tez-tez ordered locking — "sıralı bloklama" deyilir.
Nümunə
Təhlükəli kod:
// Thread 1
lock (resA)
{
lock (resB)
{
// ...
}
}
// Thread 2
lock (resB)
{
lock (resA)
{
// ...
}
}
Burada deadlock mümkündür: birinci thread resA-ni götürüb resB-ni gözləyir, ikinci isə əksinə. Hər ikisi ilişib qalır və heç vaxt azad olmur.
Düzgün kod:
// Həmişə: əvvəlcə resA, sonra resB (heç vaxt tərs)
lock (resA)
{
lock (resB)
{
// ...
}
}
İndi ardıcıllıq vahiddir və deadlock mümkün deyil. Resurslar nə qədər çox olursa-olsun və adları necə olursa-olsun — əsas odur ki, bütün thread-lər eyni sıranı izləsin.
Tipik səhv: kodda ən azı bir yerdə ardıcıllıq pozularsa, deadlock riski geri qayıdır. Buna görə diqqətli olun!
Bir vaxtda bir neçə resursu bloklamaqdan çəkinin
Bu ən sadə yanaşmadır: əgər eyni anda iki və ya daha çox lock almaq lazım deyilsə — almaqdan çəkinin! Nə qədər çox nested lock olsa, ilişib qalmaq riski bir o qədər artır. Əvəzinə lazım olan məlumatları müvəqqəti dəyişənlərə kopyalayıb lock-u buraxmaq və sonra başqa işlər görmək olar.
Lock alarkən timeout-lardan istifadə edin
Əgər həqiqətən bir neçə lock almaq lazımdırsa, timeout-lar köməyə gəlir. Məsələn, adi lock yerinə "sınama" imkanı verən metodlardan istifadə edin; alınmazsa hər şeyi geri qaytardan mexanizm tətbiq edin — məsələn, Monitor.TryEnter.
object resourceA = new object();
object resourceB = new object();
bool successA = false, successB = false;
try
{
// Hər iki lock-u maksimum 2 saniyə içində almağa çalışırıq
successA = Monitor.TryEnter(resourceA, TimeSpan.FromSeconds(2));
if (!successA) return; // alınmadı, çıx
successB = Monitor.TryEnter(resourceB, TimeSpan.FromSeconds(2));
if (!successB) return;
// Kritik bölmə
}
finally
{
if (successB) Monitor.Exit(resourceB);
if (successA) Monitor.Exit(resourceA);
}
Əgər ikinci lock-u 2 saniyə içində almaq alınmazsa, ilk lock-u buraxırıq və çıxırıq. Nəticədə deadlock baş vermir, sadəcə bəzi thread-lər cəhdlərini geri götürür.
Lock içindəki kodun həcmını minimallaşdırın
Lock altında olan kod nə qədər az olarsa, digər yolun gözləmə vaxtı bir o qədər qısa olur. Komple hesablamalar, network çağırışları və disk əməliyyatlarını kritik bölmənin içində etmək məsləhət deyil. Lock-u alın — ümumi resursu tezcə dəyişin — buraxın. Qalanını xaricdə edin.
Vəziyyəti bölün — shared state-dən qaçının
Bəzən lock-larla mübarizə aparmaq yerinə ümumi state-dən sadəcə qaçmaq daha yaxşıdır:
- Immutable obyektlərdən istifadə edin.
- Global yerinə copy və ya local dəyişənlərlə işləyin.
- Mesajlarla ötürmə (Actor Model, message queue) tətbiq edin.
- Thread-safe kolleksiyalardan istifadə edin: ConcurrentDictionary, ConcurrentQueue və s. — bunlar System.Collections.Concurrent-dan gəlir.
3. Deadlock baş veribsə nə etmək olar
Əl ilə öldürüb yenidən başlatmaq (ən yaxşı variant deyil)
Ən sadə yol — bütün ilişən prosesləri dayandırmaq. Lakin bu məlumat itkisi riskini gətirir. Bu son çarə olmalıdır.
İş zamanı deadlock-un aşkarlanması
Bəzi sistemlər deadlock-u avtomatik aşkar edə bilir. Əgər thread lock-u çox uzun müddət ala bilmirsə, o, hadisəni log edə, monitoring-ə bildiriş göndərə və bərpa prosesini başlada bilər.
if (!Monitor.TryEnter(resource, TimeSpan.FromSeconds(10)))
{
Console.WriteLine("Görünür deadlock baş verib və ya kimdənsə lock çox uzun saxlanılır!");
// Bərpa başlada, dump çıxara və ya istifadəçini xəbərdar edə bilərik
}
Distributed sistemlərdə gözləmə qrafikini analiz edən xüsusi detector-lar olur və ilişmiş transaction-ları məcburən unlock edə bilərlər.
Avtomatik rollback və retry
Praktik yanaşma — "təsəvvür et, təslim ol və yenidən cəhd et": əgər bütün lock-ları müəyyən vaxt içində almaq alınmırsa, geri çəkil, bir qədər gözlə (bəzi hallarda cəhdləri ayrılaşdırmaq üçün random delay) və təkrar et.
for (int attempt = 0; attempt < 3; attempt++)
{
bool gotRes1 = Monitor.TryEnter(res1, TimeSpan.FromSeconds(2));
bool gotRes2 = false;
try
{
if (gotRes1)
{
gotRes2 = Monitor.TryEnter(res2, TimeSpan.FromSeconds(2));
if (gotRes2)
{
// Kritik bölmə
break;
}
}
}
finally
{
if (gotRes2) Monitor.Exit(res2);
if (gotRes1) Monitor.Exit(res1);
}
// Alınmadı — gözləyib yenidən cəhd et
Thread.Sleep(500 + new Random().Next(500));
}
4. Ən yaxşı təcrübələr və tövsiyələr
- Lock alınma sırasını analiz edin. Giriş nöqtələrini toplayın və ardıcıllığı yoxlayın.
- Mümkünsə System.Collections.Concurrent-dan kolleksiyalardan istifadə edin.
- Code analysis alətlərindən istifadə edin. Rider və ya Visual Studio kimi IDE-lər nested lock-ları tapmağa kömək edir.
- Uzun lock cəhdlərini loglayın.
- Multi-thread komponentləri stress-test edin.
- Lock sırası haqqında konvensiyaları sənədləşdirin. Şərhlər ardıcıllıq pozuntularından qoruyur.
5. Praktik nümunələr
Nümunə: Verilənlər bazası
RDBMS-lərdə deadlock tez-tez olur: iki tranzaksiya eyni cədvəlləri fərqli sırada (A→B və B→A) update edir. Əksər DBMS-lər belə situasiyaları detekt edir və bir tranzaksiyanı "öldürür".
Nümunə: Asinxron metodlar
.NET-də async və lock-ları qarışdırmaq qarşılıqlı bloklamaya gətirə bilər: await lock-u saxlayan thread-i bloklaya bilər, başqa thread isə eyni resursu gözləyə bilər. Kritik bölmələrin sərhədlərini planlayın və onların içində await-dən qaçının.
6. Lock-larla işləyərkən tipik səhvlər
Səhv №1: string üzərində lock.
lock (myString) — pis ideyadır. .NET-də stringlər intern edilir, yəni əslində global string cədvəlini bloklayırsınız.
Səhv №2: müxtəlif yerlərdə fərqli lock sırası.
Əgər proqramın müxtəlif yerlərində lock-lar fərqli sırada alınırsa, deadlock ehtimalı kəskin artır.
Səhv №3: çox uzun lock-lar.
Lock-u lazım olandan daha uzun saxlamaq və ya kritik bölmənin içində xarici metodları çağırmaq — ilişmələrə və qarşılıqlı bloklamalara aparan yoldur.
GO TO FULL VERSION