1. Niyə adi lock yetərli olmaya bilər?
Gəlin düşünək: bizim ümumi resursumuz var — məsələn, müştərilərin siyahısı, onu çoxaxınlı tətbiqimizdə bir yerdə saxlayırıq. Çox vaxt biz sadəcə bu siyahını oxuyuruq — məsələn, interfeysdə istifadəçiyə göstəririk, hesabatlar qururuq, məlumatı filtr və sort edirik. Yalnız nadir hallarda kimsə yeni müştəri əlavə edir və ya köhnəni silir.
Əgər biz adi lock istifadə etsək, oxumaq istəyən hər bir axın digərinin oxuması və ya daha pis halda yazması bitənə qədər gözləməli olacaq. Və əgər əməliyyatların təxminən 99%-i oxumağa düşürsə? Belə çıxır ki, biz bütün oxuyucuları lazımsız yerə yavaşladırıq, halbuki onlar paralel işləyə bilərdilər.
Elə burada xüsusi bir tip bloklama — Reader-Writer Lock köməyə gəlir. Onun əsas ideyası budur: bir çox axın eyni anda oxuya bilər, amma kimsə dəyişiklik etmək istəyirsə, o eksklüziv giriş götürür və oxuyucuları bloklayır.
2. Nəzəriyyə: ReaderWriterLockSlim nədir
Siniflər haqqında qısa
- ReaderWriterLock — daha köhnə, yavaş, deadlocklara gətirə bilər. Yalnız .NET Framework 2.0 dəstəyi lazımdırsa istifadə edin.
- ReaderWriterLockSlim — sürətli müasir alternativ (Slim: “yüngül”, “nazik”). Bu sinif tez-tez rəqabət aparan oxuma/yazma əməliyyatları üçün optimallaşdırılıb və çoxlu axınları olan, oxumanın yazmadan daha tez-tez baş verdiyi tətbiqlərdə istifadə üçün tövsiyə olunur.
İş prinsipi
ReaderWriterLockSlim üç bloklama rejimini həyata keçirir:
- Read Lock (oxumaq üçün blok): bir çox axın eyni zamanda oxuya bilər.
- Write Lock (yazmaq üçün blok): yalnız bir axın məlumatları dəyişə bilər və heç kim oxuya bilməz.
- Upgradeable Read Lock (yüksəldilə bilən oxuma bloku): yalnız bir axın eyni zamanda bu cür bloğu saxlaya bilər; o oxuyur, amma lazım olsa write lock-a “yüksələ” bilər.
Bloklama sxemi
| Oxuyan axın | Yazan axın | Upgradeable Read Lock olan axın | |
|---|---|---|---|
| Oxuyan | ✅ | ⛔ | ✅/⛔ |
| Yazan | ⛔ | ⛔ | ⛔ |
| Upgradeable Read | ✅* | ⛔ | ⛔ |
✅ — icazə verilir
⛔ — bloklanır
— yalnız əgər upgradeable read lock write-ə “yüksəlməyibsə”
Həmin vaxt istifadə etmək və etməmək lazımdır
Tövsiyə olunur:
- Çox eyni vaxtda axınlar var və onlar əsasən ümumi məlumatları oxuyurlar (loqikat, cache, konfiqurasiyalar).
- Yazı nadir hallarda baş verir (məsələn, saniyədə/dəqiqədə/saatda bir dəfə).
- Oxuma performansını maksimum saxlamaq vacibdirsə və axınlara mane olmamaq lazımdır.
Məsləhət deyil:
- Yazma/oxuma əməliyyatları təxminən bərabərdirsə (adi lock daha yaxşı olar).
- Məlumatlar tez-tez dəyişirsə (gəlir azdır).
3. ReaderWriterLockSlim-in praktiki istifadəsi
Gəlin tədris tətbiqimizi genişləndirək! Əvvəl sadə müştəri kataloqu qurmuşduq və kolleksiyaya çoxaxınlı giriş yoxlamışdıq. İndi çətinliyi artırırıq: təsəvvür edin ki, bizim müştərilər siyahımız var və ona bir neçə axın müraciət edir (məsələn, bir servis yeni aksiya barədə hamıya məlumat verir, digər axın isə yeni müştərilər əlavə edir).
Əvvəlcə müştəri kolleksiyamızı təsvir edək:
// Müştəri sinfi — keçmiş mühazirələrdən tanış qəhrəman
public class Client
{
public int Id { get; set; }
public string Name { get; set; }
}
İndi ReaderWriterLockSlim vasitəsilə axın təhlükəsiz giriş üçün bir qabıq yaradaq:
using System;
using System.Collections.Generic;
using System.Threading;
public class ClientDirectory
{
private readonly List<Client> _clients = new List<Client>();
// Oxuma/yazma üçün Slim-lock
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
// Müştəri əlavə etmək (write lock)
public void AddClient(Client client)
{
_lock.EnterWriteLock(); // Yalnız bir axın müştəri əlavə edə bilər
try
{
_clients.Add(client);
Console.WriteLine($"[Axın {Thread.CurrentThread.ManagedThreadId}] Müştəri əlavə olundu {client.Name}");
}
finally
{
_lock.ExitWriteLock();
}
}
// Müştərilər siyahısının surətini almaq (read lock)
public List<Client> GetClients()
{
_lock.EnterReadLock(); // Bir çox axın eyni zamanda oxuya bilər
try
{
// Orijinalı kimsə səhvən dəyişməsin deyə surət qaytarırıq
return new List<Client>(_clients);
}
finally
{
_lock.ExitReadLock();
}
}
}
4. Çoxaxınlı oxuma və periodik yazı
Tutaq ki, 5 axın periodik olaraq bütün müştəriləri oxuyur (“hesabatlar qurur” və ya sadəcə maraqlanır kimlər var), və ayrıca bir axın bəzən yeni müştəri əlavə edir.
using System.Threading;
class Program
{
static void Main()
{
var directory = new ClientDirectory();
// Müştəri əlavə edən axın
var writerThread = new Thread(() =>
{
for (int i = 1; i <= 5; i++)
{
directory.AddClient(new Client { Id = i, Name = $"Müştəri {i}" });
Thread.Sleep(700); // Uzun əməliyyatın imitasiya edilməsi
}
});
// Bir neçə oxuyan
for (int j = 0; j < 5; j++)
{
int readerId = j + 1;
new Thread(() =>
{
for (int k = 0; k < 10; k++)
{
var clients = directory.GetClients();
Console.WriteLine($"[Oxuyan {readerId}][Axın {Thread.CurrentThread.ManagedThreadId}] Müştəri sayı: {clients.Count}");
Thread.Sleep(200); // Yazmadan tez-tez oxuyuruq
}
}).Start();
}
writerThread.Start();
writerThread.Join();
// Oxuyanlara sonlandırmaq üçün vaxt verək:
Thread.Sleep(3000);
}
}
Demək olar ki, həmişə bir neçə axın eyni vaxtda müştərilər siyahısını oxuya bilər (bir-birini gözləmədən), amma axın müştəri əlavə etdikdə — digər axınlar yazı bitənə qədər gözləməlidir. Bu oxuyucuları lazımsız bloklamadan çoxlu istəkləri idarə etməyə imkan verir.
5. Yüksəldilə bilən blok: UpgradeableReadLock
Bəzən belə olur: axın məlumatı oxuyur və yalnız bəzən, əgər şərt ödənirsə, onu dəyişmək istəyir. Görünür ki, read lock götürmək, şərti yoxlamaq, sonra read lock-dan çıxıb write lock almaq kifayətdir. Amma “aralıqda” başqa bir axın artıq məlumatı dəyişə bilər! Burada yüksəldilə bilən blok köməyə gəlir.
Onu istifadə etmək asandır:
public bool AddClientIfNotExists(string name)
{
_lock.EnterUpgradeableReadLock(); // Yalnız bir axın belə blok saxlayır
try
{
bool exists = _clients.Exists(c => c.Name == name);
if (!exists)
{
_lock.EnterWriteLock(); // Write lock-a yüksəldirik
try
{
_clients.Add(new Client { Name = name });
return true;
}
finally
{
_lock.ExitWriteLock();
}
}
return false;
}
finally
{
_lock.ExitUpgradeableReadLock();
}
}
İlk öncə axın oxuyur, əgər dəyişiklik lazımdırsa — o səviyyəni yüksəldir, siyahını digər axınlara buraxmadan.
6. Faydalı incəliklər
Müqayisə: ReaderWriterLockSlim vs lock
|
|
|
|---|---|---|
| Eyni vaxtda oxuma | Xeyr | Bəli |
| Eyni vaxtda yazma | Xeyr | Xeyr |
| Yüksəldilə bilən oxumanın dəstəyi | Xeyr | Bəli (EnterUpgradeableReadLock) |
| Sürət | Çox sürətli | Oxumalar tez-tezdirsə daha sürətli |
| Yaddaş | Minimum | Bir az daha çox |
| Sadəlik | Asandır | Biraz daha mürəkkəbdir, incə məqamlar var |
Real layihələrdə necə görünür
Böyük tətbiqlərdə, yüzlərlə axın tərəfindən oxunan, amma nadir hallarda dəyişdirilən böyük cədvəllər və ya cache-lər varsa, ReaderWriterLockSlim oxuma zamanı “tıxanmanı” aradan qaldırmaq üçün yoludur. Məsələn:
- Microservices-lərin istifadə etdiyi konfiqurasiya cache-i.
- Maliyyə hesablamaları üçün istinadların saxlanması.
- Mesaj routinqi üçün daxili cache.
Qeyd edim — əksər hallarda, xüsusən çoxaxınlılığa yeni başlayırsınızsa, adi lock daha sadə və təhlükəsizdir. ReaderWriterLockSlim isə yalnız oxumağın həqiqətən tez-tez olduğu hallarda lazım olan həllərdir.
Vizualizasiya: giriş sxemi
flowchart TD
A[Oxuyan axın 1] --Read--> D[ReaderWriterLockSlim]
B[Oxuyan axın 2] --Read--> D
C[Yazan axın] --Write--> D
D --Read icazə verilir--> E[Ümumi kolleksiyanın oxunması]
D --Write oxuma/yazmanı bloklayır--> F[Ümumi kolleksiyanın yazılması]
Yalnız oxumalar olduğu müddətcə — hamıya giriş açılır; yazı cəhdi olarsa — hamı yazının bitməsini gözləyir.
7. ReaderWriterLockSlim: incə məqamlar və tipik səhvlər
Lock Recursion (Dövrü tutma): Default olaraq ReaderWriterLockSlim eyni axının eyni tip lock-u təkrarən tutmasına icazə vermir. Yəni, əgər axın artıq read lock saxlayır və ikinci dəfə onu almağa çalışarsa — istisna atılacaq. Write lock üçün də oxşar. Lakin, UpgradeableReadLock içəridən write lock-a yüksələ bilər.
Sıraları qarışdırmayın: Əvvəl EnterUpgradeableReadLock() → onun içində EnterWriteLock() çağırın. Amma əksinə etməyin!
İstisnalar: Əgər lock-u sərbəst buraxmasanız (məsələn, ExitWriteLock() çağırmasanız), digər axınlar həmişə gözləyəcək. Buna görə həmişə try { ... } finally { ... } konstruksiyalarından istifadə edin.
Uzun müddət saxlamayın: Lock daxilində uzun IO əməliyyatları etməyin. Axın lock-u nə qədər uzun saxlayarsa, digərləri üçün gecikmə bir o qədər çox olar. Lock bloklarından mümkün qədər tez çıxmağa çalışın.
Qarışığı qiymətləndirin: Kiçik əməliyyatları qorumaq üçün ReaderWriterLockSlim istifadə etməyin, burada adi lock daha sadə və daha sürətli ola bilər.
GO TO FULL VERSION