CodeGym /Kurslar /C# SELF /Deadlock-un qarşısının alınması və aradan qaldırılması

Deadlock-un qarşısının alınması və aradan qaldırılması

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

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:

  1. Mutual Exclusion (Öz arası istisna): ən azı bir resurs eyni anda yalnız bir thread tərəfindən əldə edilə bilər.
  2. Hold and Wait (Saxlama və gözləmə): bir resursu əldə etmiş thread digər resursları gözləyə bilər.
  3. No Preemption (Məcburi alınmama): resurs zorla thread-dən alınmır — yalnız həmin thread onu azad edə bilər.
  4. 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.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION