1. C#-da Deadlock nümunəsi
Deadlock — bir qrup thread-in hər birinin bir resursu tutduğu və başqa bir thread-in tutduğu resursun azad edilməsini gözlədiyi vəziyyətdir — və nəticədə heç kim gözlədiyinə çatmır.
Gəlin praktikada baxaq. Gəlin iki lock-obyekti və iki thread olduğunu qəbul edək. Hər thread bir obyekti bloklayır, sonra ikinci obyekti götürməyə çalışır.
using System;
using System.Threading;
class Program
{
static readonly object lockerA = new object();
static readonly object lockerB = new object();
static void Main()
{
Thread thread1 = new Thread(Thread1Work);
Thread thread2 = new Thread(Thread2Work);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Hər iki thread işi bitirdi (əgər Deadlock baş verməyibsə)");
}
static void Thread1Work()
{
lock (lockerA)
{
Console.WriteLine("Thread 1: lockerA götürdü");
Thread.Sleep(100); // İkinci thread-ə lockerB götürmək şansı veririk
lock (lockerB)
{
Console.WriteLine("Thread 1: lockerB götürdü");
}
}
}
static void Thread2Work()
{
lock (lockerB)
{
Console.WriteLine("Thread 2: lockerB götürdü");
Thread.Sleep(100); // Birinci thread-ə lockerA götürmək şansı veririk
lock (lockerA)
{
Console.WriteLine("Thread 2: lockerA götürdü");
}
}
}
}
Bu kodu bir neçə dəfə işə salın — demək olar ki, mütləq proqramın «asılmasını» görəcəksiniz. Deadlock baş verdi! Hər thread öz lock-unı tutub və indi digərinin ikinci lock-u azad etməsini gözləyir.
Deadlock-un vizuallaşdırılması
Bu şəkildə görünür:
sequenceDiagram
participant Thread1 as Thread 1
participant Thread2 as Thread 2
participant lockerA
participant lockerB
Thread1->>lockerA: lockerA götürür
Thread2->>lockerB: lockerB götürür
Thread1->>lockerB: lockerB-nin azad edilməsini gözləyir
Thread2->>lockerA: lockerA-nın azad edilməsini gözləyir
Nəticədə: Thread 1 lockerA-nı tutur və lockerB-ni gözləyir, Thread 2 isə lockerB-ni tutur və lockerA-nı gözləyir. Heç kim güzəştə getmir — proqram asılıb qalır.
2. Deadlock necə aşkar etmək olar?
Niyə belə olur?
Səbəbi anlamaq üçün bir neçə komponentə baxaq:
- Çoxlu bloklama: Bir thread eyni anda bir neçə lock-u tutur.
- Səhv bloklama ardıcıllığı: Əgər thread-lər lock-ları fərqli ardıcıllıqla götürürlərsə, deadlock yarana bilər.
- Gözləmə imkanı: Thread başqa thread-in tutduğu resursun azad edilməsini gözləyir.
- Məcburi götürmə yoxdur: Bir thread başqa thread-dən lock-u zorla ala bilməz — C#-da yalnız gözləmək qalır.
Klassik «tələ»: N thread və N resurs varsa və thread-lər onları fərqli ardıcıllıqla bloklayırsa — deadlock təhlükəsi böyükdür.
Deadlock-un tipik əlamətləri
Deadlock xaricdən adi «proqramın asılması» kimi görünür. Thread-lər aktivdir, amma resurslara çıxış üçün bir-birlərini gözləyirlər.
Tipik əlamətlər:
- Proqram birdən cavab verməyi dayandırır (bəzən yalnız müəyyən yüklənmədə).
- Debugger göstərir ki, thread-lər lock, Monitor.Enter, WaitOne, EnterReadLock və ya oxşar sinxronizasiya əməliyyatlarında ilişib qalıb.
- Sistem alətləri (məsələn, Task Manager və ya Rider/Visual Studio alətləri) 0% CPU istifadə göstərir — «hər şey dayanıb».
- Log-da — heç bir xəta yoxdur, amma aktivlik də yoxdur.
Əgər JetBrains Rider və ya Visual Studio istifadə edirsinizsə, debugger ilə baxın, thread-lər harada ilişib qalıb (Stack Trace). Əgər görsəniz ki, bir neçə thread bir-birinə lock/Mutex.WaitOne ilə bağlıdır — təbriklər (və ya daha çox rəhmimiz sizə): bu deadlock-dur!
Deadlock-un klassik ssenariləri
Sazlaşmayan bloklama ardıcıllığı. Yuxarıdakı nümunədə olduğu kimi: Thread 1 lockerA-nı, sonra lockerB-ni tutur; Thread 2 isə əksinə. Tələnin hazırdır.
Yerləşmiş bloklamalar. Bir lock blokunda başqa bir obyekt üzrə yeni lock etmək.
Metodlar arasında kəsişən resurslar. Ən çətini odur ki, lock-lar müxtəlif metodlarda/klaslarda dağınıqdır. Daha çox lock olduqda ssenarilər daha da mürəkkəbləşir.
3. Deadlock-un qarşısını almaq
Yaxşı xəbər: deadlock-dan qorunmaq mümkündür!
Pis xəbər: çoxsözlü kodu dizayn edərkən diqqətli olmaq lazımdır.
Budur bir neçə praktik tövsiyə (bunları yadda saxlayın — müsahibədə müsbət təsir edəcək!):
Həmişə lock-ları eyni ardıcıllıqla götürün
Əgər bir neçə obyekt birlikdə bloklana bilirsə — ümumi ardıcıllıq müəyyən edin (məsələn, ad üzrə və ya indeksə görə) və həmişə ona riayət edin.
Nümunə — lock-ları düzgün götürmə
static void SafeLock(object objA, object objB)
{
// Referensləri müqayisə edirik — thread-lər həmişə obyektləri eyni ardıcıllıqla bloklasınlar
object first = objA.GetHashCode() < objB.GetHashCode() ? objA : objB;
object second = objA.GetHashCode() < objB.GetHashCode() ? objB : objA;
lock (first)
{
lock (second)
{
// kritik bölmə
}
}
}
Burada — hər iki thread əvvəlcə lock(objA), sonra lock(objB) daxil olacaq, əgər objA «kiçikdirsə» objB-dən.
Timeout-lardan istifadə edin
Əgər thread müəyyən vaxt ərzində lock-u ala bilmirsə — istisna atsin və ya nəzakətlə çıxsın. Bunun üçün Monitor.TryEnter uyğundur.
Monitor.TryEnter ilə nümunə
bool lockTakenA = false;
bool lockTakenB = false;
try
{
Monitor.TryEnter(lockerA, 500, ref lockTakenA);
if (!lockTakenA)
{
// 500 ms ərzində lock almaq alınmadı — deadlock-a girmədən imtina edirik
return;
}
Monitor.TryEnter(lockerB, 500, ref lockTakenB);
if (!lockTakenB)
{
return;
}
// Kritik bölmə...
}
finally
{
if (lockTakenB)
Monitor.Exit(lockerB);
if (lockTakenA)
Monitor.Exit(lockerA);
}
Əgər lock almaq alınmazsa — ilişib qalmaq yoxdur!
Bloklamağı minimallaşdırmağa çalışın
Kritik bölmələrdə yalnız mütləq lazım olan kodu saxlayın.
Müxtəlif sinxronizasiya mexanizmlərini qarışdırmayın
Əgər eyni vaxtda lock, Mutex, Semaphore, ReaderWriterLockSlim istifadə edirsinizsə — çaşqınlıq riski və deadlock riski artır.
4. Mutex, Semaphore və ReaderWriterLockSlim ilə Deadlock
Bir-birini bloklamalar yalnız klassik lock və ya Monitor ilə deyil, digər sinxronizasiya mexanizmləri ilə də baş verə bilər.
Mutex ilə Deadlock
static Mutex mutexA = new Mutex();
static Mutex mutexB = new Mutex();
void Work1()
{
mutexA.WaitOne();
Thread.Sleep(100);
mutexB.WaitOne();
// Kritik bölmə
mutexB.ReleaseMutex();
mutexA.ReleaseMutex();
}
Əgər ikinci thread onları əks ardıcıllıqla tutarsa — deadlock olacaq.
ReaderWriterLockSlim ilə Deadlock
ReaderWriterLockSlim elastik olsa da, əgər thread artıq bir tip lock tutub başqa tip lock-almağa çalışarsa, deadlock-dan qorumur.
Məsələn, əgər thread read-lock (EnterReadLock) tutur və sonra write-lock (EnterWriteLock) almağa çalışırsa — deadlock qaçılmazdır, çünki write-lock bütün read-lock-lar azad olunana qədər əldə edilə bilməz.
5. Real tətbiqlərdə Deadlock-un qarşısını necə almaq
Gəlin demolitan tətbiqimizdə necə görünə biləcəyinə baxaq. Tutaq ki, biz internet mağazası simulyatoru yazırıq və eyni vaxtda işləyənlər:
- Anbar stoklarını yeniləyən thread-lər (yazıcılar)
- Məhsulun mövcudluğunu yoxlayan thread-lər (oxuyucular)
- Administrator (xüsusi thread), həm oxuyur, həm yazır
Pis:
lock (stockLock)
{
// anbarda yeniləmə
lock (userLock)
{
// istifadəçilərlə bağlı bir şey
}
}
// ... və haradasa əksinə!
lock (userLock)
{
lock (stockLock) { }
}
Yaxşı:
Razılaşın — hər zaman əvvəlcə stockLock-u, sonra userLock-u bütün thread-lərdə bloklamağa çalışın.
6. İşdə və müsahibədə Deadlock-un qarşısını necə almaq
Real layihələrdə:
- Dizayn vaxtı — thread və lock sxemini çəkin (kağızda/taxtada olar).
- Lock ardıcıllığı barədə komanda ilə razılaşın və paylaşın.
- Statik analiz alətlərindən istifadə edin (Rider/Visual Studio, ReSharper) — onlar potensial deadlock-ları tapmağa kömək edir.
- Microsoft Docs-da Deadlock haqqında sənədləri oxuyun.
Müsahibədə:
- Problemi və onun klassik simptomlarını başa düşdüyünüzü göstərin.
- Əsas qorunma yollarını qeyd edin: vahid ardıcıllıq, timeout-lar (TryEnter), minimal kritik bölmələr.
- TryEnter nümunəsini gətirin və niyə əldə edilən bütün resursları finally-də buraxmağın vacib olduğunu izah edin.
7. Tipik səhvlər və gözlənilməz Deadlock-lar
Səhv №1: üçüncü tərəf kitabxanalarında gizlənmiş gözəçarpmayan lock-lar.
Klassik nümunə — kritik bölmədə log etmək. Thread ilişir, amma siz başqa kodda problem olduğunu anlamaya bilərsiniz.
Səhv №2: eyni resursa təkrarən bloklama.
Bu reentrant deadlock ola bilər: thread artıq tutduğu lock-a yenidən daxil olmağa çalışır, amma mexanizm təkrar giriş dəstəkləmirsə problem yaranır.
Səhv №3: kritik bölmələrdə asynchronous metodlardan istifadə.
Xüsusilə await ilə təhlükəlidir: thread idarəni ən uyğunsuz anda buraxa və bütün sistemi bloklaya bilər.
GO TO FULL VERSION