CodeGym /Kurslar /C# SELF /Bir-birini bloklama: Deadlocks

Bir-birini bloklama: Deadlocks

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

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.

2
Tapşırıq
C# SELF, səviyyə, dərs
Bağlanıb
Deadlock nümayişi
Deadlock nümayişi
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION