CodeGym /Kurslar /C# SELF /Bloklanmalar: lock və...

Bloklanmalar: lockMonitor sinfi

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

1. Giriş

Thread-lərlə işləməkdə tanış vəziyyəti nəzərdən keçirək. Tutaq ki, bizim çox sadə tətbiqdə ümumi uğurlar sayğacı var.

int counter = 0;

void IncrementCounter()
{
    for (int i = 0; i < 100_000; i++)
    {
        counter++; // Atomik deyil!
    }
}

// İki thread işə salırıq:
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine($"Counter: {counter}");

Bu kodu bir neçə dəfə işə salın. Təxminən heç vaxt 200_000 görməyəcəksiniz! Niyə? İki thread bir-birinə daim mane olur, bəzən hər ikisi eyni vaxtda dəyişəni oxuyur, artırır — və eyni nəticəni yazır. Nəticədə bəzi inkrementlər "itirilir".

Bu yarış vəziyyətidir, yəni Race Condition. Qaydalar, "növbə" yoxdursa, thread-lər məlumat üçün demək olar ki, dava edir.

Kritik seksiya: nədir?

Kritik seksiya — eyni anda yalnız bir thread-in icra etməli olduğu kod hissəsidir. Mətbəx analoquna qayıdanda: bu açıq kran kimidir — əgər iki nəfər bir lavaboda üzünü yumağa çalışsa, hər yer su və diş pastası olacaq. Razılaşaq ki, hamamı tək-tək istifadə edək!

Bizim nümunədə kritik seksiya — sətirdir counter++.

2. lock açar sözü

C#-də kritik seksiya yaratmaq üçün qısa və təhlükəsiz yol var — lock açar sözü. O, sinxronizasiya primitivləri ilə mürəkkəb işi gizlədir və qorunan kod blokuna bir dəfə yalnız bir thread-in daxil olmasını təmin edir.

lock-dan necə istifadə etmək

Sintaksis:

lock (lockerObject)
{
    // Yalnız bir threadin eyni anda icra edə biləcəyi kod
}

lockerObject — proqramın ömrü boyunca mövcud olan hər hansı obyekt ola bilər. Adətən belə edirlər:

private static object locker = new object();

Nəzərə alın: heç vaxt bloklama üçün stringlərdən, ədədlərdən və ya başqalarının təsadüfən əlinə keçə biləcək obyektlərdən istifadə etməyin! Yalnız özəl (private) obyektlər istifadə edin ki, başqa heç yerdə istifadə olunmasın.

Nümunəmizi düzəldək

private static object locker = new object();
int counter = 0;

void IncrementCounter()
{
    for (int i = 0; i < 100_000; i++)
    {
        lock (locker)
        {
            counter++; // İndi bu atomikdir!
        }
    }
}

İndi iki və ya on thread bu kod hissəsinə növbə ilə daxil olacaq. Nəticə ideal 200_000 olacaq. Pişiklər məmnundurlar!

3. lock-un içəridə necə işlədiyi? Monitor sinfi

İçəridə lock System.Threading.Monitor sinfi ilə işləyir. Bu, xüsusi giriş icazəsi verən katib kimidir.

Sintaksis, lock-a ekvivalent (amma daha "açıq"):

Monitor.Enter(locker);
try
{
    // Kritik seksiya
}
finally
{
    Monitor.Exit(locker);
}

Əsas fərq — özünüz təmin etməlisiniz ki, Monitor.Exit çağırılsın. Adətən buna görə try...finally lazımdır. Əgər Exit() çağırmağı unutmusunuzsa, thread "içəridə" qalar və digər thread-lər əbədi gözləyəcək — proqram köhnə Windows kimi update zamanı donacaq.

Cədvəl: lock vs manual Monitor

Metod Səhvlərə qarşı təhlükəsizlik Yazmaq daha asandır Elastiklik
lock(obj)
Bəli Bəli Xeyr
Monitor
Yalnız try/finally ilə Xeyr Bəli

99% halda lock-dan istifadə edin. Əl ilə Monitor yalnız maksimum elastiklik lazım olduqda lazımdır: məsələn, lock metodunu timeout ilə etmək istəyəndə.

4. lock üçün arqumentlər: nələrə icazə var, nələri etmək olmaz?

Yeni başlayanların tez-tez etdiyi səhv: bloklama üçün string və ya başqa "görünən" obyekt istifadə etmək. Məsələn:

lock ("mylock") { /*...*/ } // Çox pis!

Problemin mahiyyəti odur ki, stringlər intern edilir (tətbiq üzrə unikaldır), başqa kitabxanalarla toqquşma asandır və nəticədə proqram "ölü" vəziyyətə düşə bilər. Həmişə özəl obyektlərdən istifadə edin:

private readonly object myLock = new object();

lock (myLock)
{
    // yalnız sizin kod myLock haqqında bilir
}

5. lock: konsola yazma nümunəsi

Gəlin bacarıqları məşq edək! Mini-tətbiq yazacağıq: iki thread sətirlər çap edir, amma konsola çıxış da sinxronlaşdırılıb — mətn qarışmasın.

private static object consoleLock = new object();

void PrintMessages(string name)
{
    for (int i = 0; i < 5; i++)
    {
        lock (consoleLock)
        {
            Console.WriteLine($"{name}: Mesaj {i + 1}");
            Thread.Sleep(50); // Emulyasiya üçün işlənmə
        }
    }
}

Thread t1 = new Thread(() => PrintMessages("Thread 1"));
Thread t2 = new Thread(() => PrintMessages("Thread 2"));

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Nəticə: sətirlər ardıcıl gəlir, heç bir qarışıqlıq yoxdur. Bu yanaşma tez-tez logging üçün istifadə olunur ki, loglarda "krakozabralar" olmasın.

6. Faydalı nüanslar

Bloklamanın əl ilə idarəsi: irəliləmiş Monitor

Default lock kifayət etmədikdə (məsələn, kritik seksiyaya daxil olmağa cəhd edib əbədi gözləmək istəmirsinizsə) Monitor.TryEnter metodu kömək edir.

if (Monitor.TryEnter(locker, 100)) // 100 ms gözləmə
{
    try
    {
        // Kritik seksiya
    }
    finally
    {
        Monitor.Exit(locker);
    }
}
else
{
    Console.WriteLine("100 millisekund ərzində bloklanmanı ala bilmədik");
}

Bu, proqramınızın "donmasını" istəmədiyi hallarda rahatdır — məsələn, istifadəçiyə mesaj verə və ya başqa faydalı iş görə bilərsiniz, ümumi resurs işğal edildiyi zaman.

Vizualizasiya: bloklanma necə işləyir (sxem)

flowchart LR
    A[Thread 1: kritik seksiyaya daxil olmaq istəyir]
    B[Thread 2: kritik seksiyaya daxil olmaq istəyir]
    C[locker boşdur]
    D[Thread 1 lock içində kodu icra edir]
    E[Thread 2 gözləyir]
    F[Thread 1 lock-dan çıxdı]
    G[Thread 2 giriş alır]
    
    A -- locker yoxlanışı --> C
    C -- locker boşdur --> D
    B -- locker yoxlanışı --> D
    D -- lock tutuldu --> E
    D -- iş bitdi --> F
    F -- locker azad edildi --> G
    E -- locker indi boşdur --> G

Bloklanmalar və performans

Bloklanmalar sadədir: eyni anda yalnız bir thread qıvrıcı mötərizələr arasındakı kodu icra edə bilir. Bu məlumat bütövlüyü üçün yaxşıdır, amma... nə qədər çox thread "növbədə" durursa, o qədər hamısı yavaşıyır. Odur ki, sinxronizasiya universal dərman deyil: mümkün qədər kiçik kritik seksiyalar ayırmağa çalışın.

Həyat hiyləsi: əgər kritik seksiyanın icrası millisaniyənin törəməsidirsə — yaxşıdır. Əgər orada uzun hesablamalar, I/O, şəbəkə və ya fayl işi varsa — onları lock-dan kənara çıxarın. Əvvəl oxu/hesablama edin, sonra sürətlə ümumi vəziyyəti lock daxilində yeniləyin.

Müsahibədə və reallıqda

Hər ciddi proqramda, harda thread-lər istifadə olunursa, işə götürənlər mütləq soruşacaq: "İki thread eyni dəyişənə müraciət edərsə nə edirsiniz?" Bloklama ilə kod göstərin — resume-niz HR-robotunun qara dəstəsinə düşməyəcək.

Amma real dünyada, yüksək yüklü sistemlərdə daha inkişaf etmiş sinxronizasiya mexanizmləri də işlənir — lakin lockMonitor sadə hallar üçün hələ də qızıl standartlardır.

7. Bloklanmaların istifadə xüsusiyyətləri və tipik səhvlər

Ən çox rastlanan səhv — eyni məqsəd üçün eyni obyektə lock tətbiq etməməyi unutmaq. Məsələn:

void Foo() { lock (a) { ... } }
void Bar() { lock (b) { ... } }

Əgər hər iki metod eyni dəyişəni idarə edirsə, amma obyektlər ab fərqlidirsə, sadəcə saxta müdafiə yaratmısınız — thread-lər eyni zamanda həmin dəyişən üzərində işləyəcək!

Nəticə: eyni məlumatı qorumaq üçün həmişə eyni obyekt istifadə edin.

Başqa hal — çox "geniş" lock etmək. Məsələn, adi klass daxilində lock (this) istifadə etmək, əgər kənardan da həmin obyektdən lock üçün istifadə ediləcəyindən əmin deyilsinizsə, təhlükəli ola bilər. Bu həmçinin deadlock və digər maraqlı, amma arzuolunmaz səhvlərə gətirib çıxara bilər.

Və nəhayət: uzun və xarici əməliyyatları (fayl, şəbəkə və s.) lock daxilində etməyin. Belə etməklə siz digər thread-lərin resurslara çıxışını uzun müddət blok edə bilərsiniz və performans azalır. Kritik seksiya = yalnız paralel icra edilə bilməyən əməliyyatlar!

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