1. «Bloklamasız» pattern (Lock-Free / Wait-Free)
Siz artıq bilirsiniz ki, Concurrent kolleksiyalar axın-təhlükəsizdir. Amma onlar bunu necə edirlər ki, siz kodunuzu lock blokları ilə örtməli olmayasınız? Cavab qabaqcıl alqoritmlərdədir — lock-free (bloklamasız) və wait-free (gözləməsiz).
Konsepsiyaların qısa izahı: lock-free və wait-free alqoritmlər
Lock-Free (bloklamasız)
Məqsəd: Zəmanət verir ki, ən azı bir axın həmişə irəliləyə biləcək, hətta digər axınlar gecikmə və ya kəsilmələrlə üzləşsə belə.
lock-dan fərqi: lock istifadə edildikdə rəqabət aparan axınlar blokun açılmasını gözləyirlər. Lock-free alqoritmlərdə axınlar klassik mənada bir-birini gözləmir: konflikt olduqda sadəcə yenidən cəhd edirlər.
Nümunə: Kassaya növbə: lock ilə siz dayanıb gözləyirsiniz. Lock-free-da yaxınlaşırsınız, görürsünüz ki, işğal olunub, və qısa vaxtdan sonra yenidən cəhd edirsiniz — ümumi növbədə "dayanmaq" olmur.
Wait-Free (gözləməsiz)
Məqsəd: Daha güclü zəmanət: hər bir axın öz adımlarının məhdud sayında irəliləyəcək, digər axınlardan asılı olmayaraq. Heç kim sonsuz dövrə düşməyəcək.
Fərq: Lock-free-da axın konfliktlər səbəbindən əməliyyatı sonsuz olaraq yenidən başlada bilər; wait-free-da belə hal olmur.
Praktika: Wait-free həyata keçirmək xeyli çətindir, buna görə çox vaxt lock-free və ya hibrid yanaşmalar istifadə olunur.
2. Concurrent kolleksiyalar axın-təhlükəsizliyə necə çatır
Lock-free alqoritmlərin əsas tikinti bloku — prosessor səviyyəsindəki atomik əməliyyatlardır. .NET-də biz bunlara System.Threading.Interlocked klassı vasitəsilə müraciət edirik.
Interlocked-əməliyyatlar
Primitivlər üzərində sürətli atomik əməliyyatlar (int, long): məsələn, Interlocked.Increment, Interlocked.Decrement, Interlocked.CompareExchange.
Nümunələr: Interlocked.Increment(ref value) — atomik artım; Interlocked.CompareExchange(ref location, value, comparand) — atomik olaraq müqayisə edir və uyğun gəlsə yeniləyir.
CAS (Compare-And-Swap) — Müqayisə-və-Əvəz
CAS əməliyyatı .NET-də Interlocked.CompareExchange kimi həyata keçirilir. Ümumi məntiq:
- Cari dəyişən dəyərini oxu.
- Oxunan əsasında yeni dəyəri hesabla.
- Yalnız əgər dəyişən hələ də ilkin dəyərə bərabərdirsə onu yazmağa cəhd et. Əgər yoxdursa — yenidən cəhd et.
Nümunə: sadə lock-free sayğac Interlocked ilə
using System.Threading;
using System.Threading.Tasks;
class CounterExample
{
static int regularCounter = 0;
static int interlockedCounter = 0;
static void IncrementRegular(int iterations)
{
for (int i = 0; i < iterations; i++)
{
regularCounter++; // Axın-təhlükəsiz deyil!
}
}
static void IncrementInterlocked(int iterations)
{
for (int i = 0; i < iterations; i++)
{
Interlocked.Increment(ref interlockedCounter); // Atomik!
}
}
}
//Main-də:
Task t1 = Task.Run(() => IncrementRegular(500_000));
Task t2 = Task.Run(() => IncrementRegular(500_000));
Task.WaitAll(t1, t2);
Console.WriteLine($"Adi sayğac: {regularCounter}"); // demək olar ki, həmişə 1_000_000-dan az olacaq
regularCounter = 0; // Növbəti test üçün sıfırlayırıq
t1 = Task.Run(() => IncrementInterlocked(500_000));
t2 = Task.Run(() => IncrementInterlocked(500_000));
Task.WaitAll(t1, t2);
Console.WriteLine($"Interlocked sayğac: {interlockedCounter}"); // Dəqiq 1_000_000 olacaq
Interlocked.Increment metodu inkremmentin atomikliyini təmin edir: çoxlu axınların eyni anda daxil olmasına baxmayaraq məlumatlar itmir.
3. Niyə bu miqyaslanma və performans üçün önəmlidir
Əlavə xərcləri azaldır: klassik kilidlər (lock) kontekst dəyişmələrinə və OS səviyyəsində gözləmələrə səbəb ola bilər. Lock-free bu xərcləri minimuma endirir.
Deadlock yoxdur: axınlar bir-birini gözləmədiyi üçün qarşılıqlı bloklanma yaranmır.
Yaxşı miqyaslanma: çoxnüvəli sistemlərdə axınlar bir-birinə daha az maneə törədir, ümumi bir kilidin yaratdığı "şiş" olmaz.
Artan reaksiya qabiliyyəti: heç kim uzun müddət gözləməyə "yığışmır".
ConcurrentQueue<T>-nin daxili strukturuna qısa baxış
Sadələşdirilmiş şəkildə: sıra əlaqəli segmentlərdən ibarətdir. Enqueue zamanı axın atomik şəkildə "tail"-i CompareExchange vasitəsilə irəli aparır; TryDequeue zamanı — "head"-i yalnız dəyişilməmişsə atomik sürüşdürür. Real implementasiyalar daha mürəkkəbdir (ABA problemini həll etmək və GC-i nəzərə almaq kimi), amma əsas — ağır kilidlər əvəzinə atomik əməliyyatlardır.
4. Concurrent kolleksiyaların performansı
lock-lu adi kolleksiyalarla performans müqayisəsi
Aşağı rəqabət zamanı fərq kiçik ola bilər, bəzən adi lock ilə qorunan kolleksiya bənzər göstərici verə bilər. Amma yüksək rəqabət zamanı Concurrent-kolleksiyalar ümumiyyətlə xeyli sürətlidir, çünki ümumi kilid üçün gözləmələr yoxdur.
Nümunə: müqayisə (ideya, işə salmadan)
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics; // Stopwatch üçün
using System.Threading.Tasks;
class PerformanceTest
{
static List<int> regularList = new List<int>();
static ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
static object lockObject = new object();
const int Iterations = 1_000_000;
const int NumTasks = 4; // Paralel tapşırıqların sayı
public static void RunTests()
{
Console.WriteLine("Performans testi (əlavə etmə):");
// Adi List və lock ilə test
regularList.Clear();
Stopwatch sw = Stopwatch.StartNew();
Parallel.For(0, NumTasks, (i) =>
{
for (int j = 0; j < Iterations / NumTasks; j++)
{
lock (lockObject)
{
regularList.Add(j);
}
}
});
sw.Stop();
Console.WriteLine($"List ilə lock: {sw.ElapsedMilliseconds} ms. Count: {regularList.Count}");
// ConcurrentQueue ilə test
concurrentQueue.Clear();
sw = Stopwatch.StartNew();
Parallel.For(0, NumTasks, (i) =>
{
for (int j = 0; j < Iterations / NumTasks; j++)
{
concurrentQueue.Enqueue(j);
}
});
sw.Stop();
Console.WriteLine($"ConcurrentQueue: {sw.ElapsedMilliseconds} ms. Count: {concurrentQueue.Count}");
// Gözləyin ki, NumTasks > 1 olduqda ConcurrentQueue xeyli sürətli olacaq
}
}
Nəticə: Əgər kolleksiyaların ətrafında lock görürsünüzsə, bu çox vaxt Concurrent analoqlarına keçmək üçün siqnal olur.
6. Faydalı nüanslar
contention-un performansa təsiri
Contention — çoxlu axınların eyni resursa eyni anda müraciət etməsidir. Rəqabət nə qədər yüksəkdirsə, o qədər çox gözləmələr yaranır və performans pisləşir.
Concurrent-kolleksiyalar contention-u azaltmaq üçün dizayn edilib: məsələn, ConcurrentBag<T> axın-yerli (thread-local) saxlama istifadə edir, ConcurrentDictionary<TKey, TValue> isə şeritli (striped) kilidləmələrdən istifadə edir.
Performansın açarı — contention-un azaldılması: mümkün qədər məlumatı axınlar arasında bölün və ya bir neçə kolleksiya istifadə edin.
Konkret ssenari üçün doğru kolleksiyanı seçmək
| Kolleksiya | Sıra | İstifadə vaxtı | İstifadə etməmək lazım olduqda |
|---|---|---|---|
|
FIFO (First-In, First-Out) | Tapşırıq sıraları, loglama, asinxron hadisə işlənməsi, Producer-Consumer. | Əgər sıralama vacib deyilsə, LIFO lazımdırsa və ya ölçüsü məhdudlaşdırılıb bloklanma tələb olunursa. |
|
LIFO (Last-In, First-Out) | Əməliyyat tarixi (Undo/Redo), qrafın DFS gəzintisi, son əlavə olunan prioritetli obyekt hovuzları. | Əgər FIFO kritikdirsə və ya sıralama stabilliyi vacibdirsə. |
|
Zəmanət olunmur | Obyekt hovuzları, xüsusən istehsalçı və istehlakçının çox vaxt eyni axın olduğu hallarda; TPL ssenarilərində lokallıq vacibdirsə. | Elementlərin sırası vacibdirsə. |
|
Yox | Keşləmə, istifadəçi sessiyaları, statistika sayımı, paralel agregasiya. | Əgər sizə dictionary lazım deyilsə. |
| BlockingCollection<T> (üstündə ConcurrentQueue) | FIFO (və ya digər əsas kolleksiyanın davranışı) | Producer-Consumer bloklayan əməliyyatlar və ölçü məhdudiyyəti, rahat sonlandırma üçün. | Əgər bloklayan əməliyyatlar və ya ölçü məhdudiyyəti tələb olunmursa. |
7. Optimizasiya məsləhətləri
«İsti» yerlərdə tez-tez ToArray()-dan çəkinin
ToArray() kolleksiyanın tam surətini yaradır — yaddaş və zaman baxımından baha başa gəlir. Yalnız "ani görüntü" lazım olduqda və mümkün qədər nadir istifadə edin. Element sayı üçün Count var (çağırış zamanı götürülən snapshot olduğunu unutmayın).
Concurrent-kolleksiyalar üzrə iterasiyaya diqqət
Iterasiya zamanı paralel dəyişikliklər zamanı stabilik zəmanət olunmur: elementlər itə bilər və ya qeyri-konsistent görünüş əldə oluna bilər. Stabil görünüş üçün əvvəlcə ToArray() ilə snapshot alın.
// Pis: elementləri itirə və ya dəyişikliklər görə bilərsiniz
foreach (var item in myConcurrentQueue) { /* ... */ }
// Yaxşı: sabit snapshot üzərində iterasiya
var snapshot = myConcurrentQueue.ToArray();
foreach (var item in snapshot) { /* ... */ }
Kolleksiya üzərindən «trafiki» minimuma endirin
Tapşırıqları/məlumatları qruplaşdırın: Add/Take çağırışları nə qədər az olsa, potensial contention bir o qədər az olar. Məsələn, 1000 ayrı mesaj əvəzinə bir 1000-li paket göndərin.
Rəqabət mənbələrini izləyin
Əgər performans düşüşü görsəniz, harada ən çox contention olduğunu ölçün. Ola bilər ki, dizaynı dəyişərək axınların lokal məlumatlarla və ya fərqli kolleksiyalarla işləməsini təşkil etmək olar.
GO TO FULL VERSION