1. Giriş
Mutex və lock — tək bir müştərini xidmət edən barista kimidir. Amma əgər bizim bir kofe maşınımız yox, üçü varsa — və eyni anda üç fincan qəhvə hazırlana bilərsə, nə etmək lazımdır?
Məsələn, üç kofe maşını olan kafen var. Müştərilər (thread-lər) gəlir, boş maşını tuturlar, qəhvəni hazırlayıb gedirlər. Əgər bütün üç maşın məşğuldursa, qalanlar boşalmağı gözləyir.
Sual: Necə etmək olar ki, eyni vaxtda maksimum üç müştəri maşınlarda işləsin, digərləri isə növbədə dayansın?
Cavab: semafordan istifadə et!
Semafor nədir?
Semafor — sinxronizasiya üçün klassik alətdir. Əgər lock/Mutex "bir daxil oldu — qalanlar gözləyir" deyirsə, semafor belə bildirir: "mən eyni vaxtda N icazə verirəm!".
Semaforlar 1965-ci ildə Edsger Dijkstra tərəfindən təklif olunub. Adı dəniz siqnalizasiyasından gəlir: bayraqlar necə məlumat verirdisə, semafor da thread-lərə deyir — daxil ola bilərsiniz ya gözləməlisiniz.
İstifadə ssenariləri
- Resursla eyni anda işləyən threadlərin sayını məhdudlaşdırmaq.
- Verilənlər bazasına eyni anda qoşulanların sayı, paralel sorğuların sayı, ağır tapşırıqlar üçün limit qoymaq.
2. Siniflərin icmalı: Semaphore və SemaphoreSlim
Semaphore
- Ağır sinifdir, kernel obyektlərindən istifadə edir.
- Fərqli proseslər arasındakı sinxronizasiyanı dəstəkləyir.
- Ad təyin edib proseslər arasında paylaşmaq olar.
SemaphoreSlim
- Yüngülləşdirilmiş versiyadır, yalnız bir proses daxilində işləyir.
- Daha sürətlidir və resurslara qənaət edir.
- Əgər proseslərarası sinxronizasiya lazım deyilsə, demək olar ki hər zaman üstünlük verilir.
Analogi: səyahət çantası (SemaphoreSlim) vs böyük çamadan (Semaphore). Yüngül səyahət edirsənsə, çantanı götür.
Müqayisəli cədvəl
| Sinif | Proseslərarası | Performans | Tövsiyə edilir |
|---|---|---|---|
|
Bəli | Daha yavaş | Proseslərarası sinxronizasiya lazım olanda |
|
Xeyr | Daha sürətli | 99% hallarda, bir proses daxilində |
Semaforun əsas metodları və xassələri
Əsas parametrlər
- InitialCount — başlanğıc icazə sayı.
- MaxCount — eyni anda verilə biləcək maksimum icazə.
Açar metodlar
- Wait() və ya WaitAsync() — giriş tələb etmək (icazəni tutmaq).
- Release() — icazəni azad etmək.
Necə işləyir
Əgər Wait() çağırışında icazə yoxdursa, thread bloklanır və kimisə Release() çağırana qədər gözləyir. İstifadəyə görə azad ediləndən sonra gözləyənlərdən biri davam edir.
3. İlk praktik nümunə
Konsol tətbiqinə 3 yerlik "parkovka" əlavə edək və 10 thread-ə işə salaq.
using System;
using System.Threading;
class Program
{
// 3 icazəli semafor (3 parkovka yeri)
static SemaphoreSlim parking = new SemaphoreSlim(3);
static void Main()
{
for (int i = 1; i <= 10; i++)
{
int carNumber = i;
new Thread(() =>
{
Console.WriteLine($"Maşın #{carNumber} parkovkaya daxil olmağa çalışır...");
parking.Wait(); // Boş yer gözləyir
Console.WriteLine($"Maşın #{carNumber} parkovkaya daxil oldu!");
Thread.Sleep(2000); // 2 saniyə parkda qalırıq
Console.WriteLine($"Maşın #{carNumber} parkovkadan çıxır.");
parking.Release(); // Yeri azad edirik
}).Start();
}
}
}
- Eyni anda yalnız üç maşın "parklanacaq".
- Qalanlar yer azalana qədər gözləyəcəklər.
- Çıxış qarışa bilər — bu çoxthreadli proqram üçün normaldır.
4. Semafor yük məhdudlaşdırıcısı kimi
Ağır tapşırıqların (məsələn, yükləmələrin) eyni anda icrasını 5-ə qədər məhdudlaşdıraq.
static SemaphoreSlim semaphore = new SemaphoreSlim(5); // maksimum 5 eyni vaxtda yükləmə
static void DownloadFile(int fileId)
{
semaphore.Wait();
try
{
Console.WriteLine($"--> Fayl {fileId} yüklənməyə başlayır");
Thread.Sleep(1000 + fileId * 100); // Yükləmə (imitasiya)
Console.WriteLine($"<-- Fayl {fileId} yükləndi");
}
finally
{
semaphore.Release();
}
}
static void Main()
{
for (int i = 1; i <= 12; i++)
{
int localId = i;
new Thread(() => DownloadFile(localId)).Start();
}
}
Vacib məqam: Wait()-i try blokundan əvvəl çağır və Release()-i finally-də qoy. Beləliklə icazə istisna baş versə belə azad olunacaq.
5. Wait(int millisecondsTimeout) və asinxron metodlar
Sadəcə müəyyən müddət gözləmək də olar:
if (semaphore.Wait(500))
{
// Yarım saniyə ərzində icazə almaq mümkün oldu!
}
else
{
// 500 ms içində gözləmədik — dayandıq
}
Müasir tətbiqlərdə (məsələn, ASP.NET) asinxron variantdan istifadə et: await semaphore.WaitAsync(). Bu, icazə gözlənilərkən thread-i bloklamır.
Qeyd: asinxron kodda məhz SemaphoreSlim və onun WaitAsync-indən istifadə et, yoxsa gözlənilməz deadlock-lar ala bilərsən.
6. Yanlış və düzgün nümunələr
Tez-tez edilən səhv — Release()-i unutmaq: icazələr "axır" və hər şey dayanır.
Pis
static void SomeWork()
{
semaphore.Wait();
// ... işləmə, amma Release unutuldu!
}
Yaxşı
static void SomeWork()
{
semaphore.Wait();
try
{
// işləmə
}
finally
{
semaphore.Release();
}
}
Asinxron variant
static async Task SomeAsyncWork()
{
await semaphore.WaitAsync();
try
{
// asinxron işləmə
}
finally
{
semaphore.Release();
}
}
7. Semaforun daxili işi (sadə dildə)
Semafor bir sayğacdır. Wait() onu 1-ə azaldır. Əgər sayğac > 0 idisə — thread keçir; əks halda 0 olarsa — thread gözləyir. Release() sayğacı artırır və gözləyənləri oyadır.
+-------------------------------+
| Semafor (sayğac = 3) |
+-------------------------------+
| [ ] [ ] [ ] | <--- İcazələr
+----+----+----+----------------+
| | |
Thread Thread Thread
8. Faydalı nüanslar
Digər primitivlərlə fərqi
- lock / Monitor / Mutex — yalnız bir thread-i buraxır (eksklüziv erişim).
- Semaphore/SemaphoreSlim — eyni anda məhdud sayda, N thread-i buraxır.
Semafor "sahibli ilə bağlı" deyil: icazəni hər hansı bir thread azad edə bilər. Bu xüsusiyyətdir, səhv deyil.
Real həyatda tətbiqlər
- Servisə və ya DB-ə paralel bağlantıların limitlənməsi.
- Pul: resursda eyni anda ən çox N thread.
- Eyni anda işlənən web sorğularının məhdudlaşdırılması.
- Yüklənmədən qorunmaq üçün oxu/yazma limitləri.
- Xarici API çağırışlarının limitlənməsi.
Səhv nümunəsi (Release daha çox çağırılıb)
var semaphore = new SemaphoreSlim(2);
semaphore.Release(); // Səhv! Sayğac 3 oldu, MaxCount-ı keçdi — SemaphoreFullException atılacaq.
Burada SemaphoreFullException yaranacaq: sayğac maksimumu keçdi.
Semaphore və SemaphoreSlim arasındakı fərqlər
- SemaphoreSlim — proses daxilidir, daha sürətli və sadədir (demək olar ki, həmişə istifadə et).
- Semaphore — proseslərarası sinxronizasiya üçün lazımdır (nadir ssenari).
Niyə semaforları bilmək lazımdır?
İntervyuda tez verilən sual: "Resursla işləyən threadlərin sayını necə məhdudlaşdırmaq olar?" — düzgün cavab: semafor.
- lock — 1 thread.
- Semaphore/SemaphoreSlim — N thread.
9. Tipik səhvlər və istifadə xüsusiyyətləri
Səhv №1: Release()-i çağırmağı unutmaq. Əgər thread icazəni tutub (Wait() və ya WaitAsync()) amma onu azad etmirsə, digərləri sonsuz müddət gözləyəcək — tətbiq "donacaq".
Səhv №2: Release()-i Wait()-dən daha çox çağırmaq. Artıq "əlavə" icazələr yaranır. Semaphore üçün bu SemaphoreFullException-a gətirib çıxaracaq və giriş məntiqini poza bilər.
Səhv №3: müxtəlif sinxronizasiya mexanizmlərini qarışdırmaq. Bir yerdə lock, başqa yerdə semafor istifadə etmək eyni resursa qarşı. Bu deadlock riskini artırır.
Səhv №4: asinxron koda Semaphore-dən istifadə etmək. Klassik semafor async/await-lə yaxşı işləmir. Asinxron ssenarilər üçün SemaphoreSlim və WaitAsync()-dən istifadə et.
Səhv №5: yanlış initialCount və maxCount seçmək. Düzgün olmayan dəyərlərlə məhdudiyyəti aşa və resursa gözləniləndən artıq thread daxil ola bilər.
GO TO FULL VERSION