30-Bob: Gibrid Thread Sinxronizatsiya Konstruktsiyalari

User-mode va kernel-mode konstruktsiyalarini birlashtirib, yuqori unumdorlikli qulflar yaratish

~60 daqiqaO'qish vaqti
15 ta mavzuShu bobda
Ilg'orDaraja
789-822 betAsl kitobda

29-bobda biz primitiv user-mode va kernel-mode thread sinxronizatsiya konstruktsiyalarini ko'rib chiqdik. User-mode konstruktsiyalari tez ishlaydi, lekin threadni bloklash imkoniyatiga ega emas. Kernel-mode konstruktsiyalari threadni bloklash imkonini beradi, lekin sekin ishlaydi chunki har bir operatsiyada OS yadrosiga o'tish kerak bo'ladi.

Bu bobda biz gibrid (hybrid) konstruktsiyalarni ko'rib chiqamiz. Bu konstruktsiyalar user-mode va kernel-mode konstruktsiyalarining eng yaxshi tomonlarini birlashtiradi. Ular threadlar o'rtasida raqobat (contention) bo'lmaganda tez ishlaydigan user-mode konstruktsiyalarini ishlatadi. Raqobat paydo bo'lganda esa, kernel-mode konstruktsiyalaridan foydalanib, threadlarni samarali bloklaydi.

Asosiy g'oya

Gibrid qulflar "eng yaxshi ikki dunyo" yondashuvini taqdim etadi: raqobatsiz holatlarda CPU tsikllarini isrof qilmasdan user-mode da ishlaydi, raqobat bo'lganda esa kernel-mode ga o'tib threadni bloklaydi — shunda CPU resurslarini boshqa ishlarga bo'shatadi.

Oddiy Gibrid Qulf (SimpleHybridLock)

Gibrid qulfning eng oddiy namunasini yaratish uchun biz user-mode konstruktsiyasi (Interlocked metodlari) va kernel-mode konstruktsiyasi (AutoResetEvent) ni birlashtiramiz. Quyidagi kodni ko'ring:

C# internal sealed class SimpleHybridLock : IDisposable { // The Int32 is used by the primitive user-mode constructs (Interlocked methods) private Int32 m_waiters = 0; // The AutoResetEvent is the primitive kernel-mode construct private AutoResetEvent m_waiterLock = new AutoResetEvent(false); public void Enter() { // Indicate that this thread wants the lock if (Interlocked.Increment(ref m_waiters) == 1) return; // Lock was free, no contention, just return // Another thread has the lock (contention), make this thread wait m_waiterLock.WaitOne(); // Bad performance hit here // When WaitOne returns, this thread now has the lock } public void Leave() { // This thread is releasing the lock if (Interlocked.Decrement(ref m_waiters) == 0) return; // No other threads are waiting, just return // Other threads are waiting, wake 1 of them m_waiterLock.Set(); // Bad performance hit here } public void Dispose() { m_waiterLock.Dispose(); } }

Enter metodini birinchi chaqirgan thread Interlocked.Increment orqali m_waiters maydoniga 1 qo'shadi. Bu thread kutayotgan threadlar yo'qligini ko'radi (qiymat 1 ga teng), shuning uchun qulfni juda tez egallaydi va qaytadi. Bu joyda hech qanday kernel-mode o'tish yo'q — bu juda tez.

Agar boshqa thread Enter ni chaqirsa, m_waiters 2 ga oshadi va bu thread AutoResetEvent da WaitOne chaqirib bloklanadi. WaitOne threadni kernel rejimiga o'tkazadi — bu katta unumdorlik zarbasi. Lekin thread allaqachon to'xtashi kerak edi, shuning uchun CPU tsikllarini spinning orqali isrof qilish o'rniga, thread to'g'ri bloklanadi.

Leave metodi ham xuddi shunday ishlaydi. Agar hech qanday thread kutmayotgan bo'lsa, Interlocked.Decrement natijasi 0 bo'ladi va metod darhol qaytadi. Agar kutayotgan threadlar bo'lsa, AutoResetEvent.Set chaqiriladi va bloklangan threadlardan bittasi uyg'otiladi.

Eslatma

Amalda Enter metodini istalgan vaqtda istalgan thread chaqirishi mumkin, chunki Enter metodi qaysi thread qulfni egallagan bo'lsa o'sha haqida ma'lumot saqlamaydi. Bu ma'lumotni qo'shish mumkin, lekin bu qulf obyektining xotirasini va Enter/Leave metodlarining unumdorligini pasaytiradi. Muallif tez ishlaydigan qulfni afzal ko'radi. Event va semaforlar ham bu ma'lumotni saqlamaydi; faqat mutexlar saqlaydi.

Spinning, Thread Egaligi va Rekursiya

Yadro (kernel) ga o'tish katta unumdorlik zarbasi berganligi va threadlar qulfni odatda juda qisqa vaqt ushlab turishlari sababli, ilovaning umumiy unumdorligini yaxshilash mumkin — thread kernel rejimiga o'tishdan oldin bir oz vaqt user rejimida aylanib (spin) turadi. Agar qulf spinning davomida bo'shasa, kernel rejimiga o'tish umuman oldini olinadi.

Bundan tashqari, ba'zi qulflar qo'shimcha xususiyatlarni qo'llab-quvvatlaydi:

  • Thread egaligi (Thread Ownership): Qulfni egallagan thread qulfni bo'shatadigan thread bilan bir xil bo'lishi kerak. Ya'ni, faqat qulfni olgan thread uni bo'shatishi mumkin.
  • Rekursiya: Bir xil thread qulfni bir necha marta egallashi mumkin. Har bir Enter chaqiruvi uchun mos Leave chaqiruvi bo'lishi kerak. Mutex bu xususiyatga ega qulf namunasidir.
Diqqat!

Threadlar Mutex obyektida kutayotganda spin qilmaydi, chunki Mutex kodi yadroda joylashgan. Bu threadning yadroga kirishi kerakligini anglatadi — faqat Mutex holatini tekshirish uchun ham.

AnotherHybridLock — Spinning, Egalik va Rekursiya bilan

Spinning, thread egaligi va rekursiyani qo'llab-quvvatlovchi gibrid qulfni quyidagicha yaratish mumkin:

C# internal sealed class AnotherHybridLock : IDisposable { // The Int32 is used by the primitive user-mode constructs (Interlocked methods) private Int32 m_waiters = 0; // The AutoResetEvent is the primitive kernel-mode construct private AutoResetEvent m_waiterLock = new AutoResetEvent(false); // This field controls spinning in an effort to improve performance private Int32 m_spincount = 4000; // Arbitrarily chosen count // These fields indicate which thread owns the lock and how many times it owns it private Int32 m_owningThreadId = 0, m_recursion = 0; public void Enter() { // If calling thread already owns the lock, increment recursion count and return Int32 threadId = Thread.CurrentThread.ManagedThreadId; if (threadId == m_owningThreadId) { m_recursion++; return; } // The calling thread doesn't own the lock, try to get it SpinWait spinwait = new SpinWait(); for (Int32 spinCount = 0; spinCount < m_spincount; spinCount++) { // If the lock was free, this thread got it; set some state and return if (Interlocked.CompareExchange(ref m_waiters, 1, 0) == 0) goto GotLock; // Black magic: give other threads a chance to run // in hopes that the lock will be released spinwait.SpinOnce(); } // Spinning is over and the lock was still not obtained, try one more time if (Interlocked.Increment(ref m_waiters) > 1) { // Still contention, this thread must wait m_waiterLock.WaitOne(); // Wait for the lock; performance hit // When this thread wakes, it owns the lock; set some state and return } GotLock: // When a thread gets the lock, we record its ID and // indicate that the thread owns the lock once m_owningThreadId = threadId; m_recursion = 1; } public void Leave() { // If the calling thread doesn't own the lock, there is a bug Int32 threadId = Thread.CurrentThread.ManagedThreadId; if (threadId != m_owningThreadId) throw new SynchronizationLockException("Lock not owned by calling thread"); // Decrement the recursion count. If this thread still owns the lock, just return if (--m_recursion > 0) return; m_owningThreadId = 0; // No thread owns the lock now // If no other threads are waiting, just return if (Interlocked.Decrement(ref m_waiters) == 0) return; // Other threads are waiting, wake 1 of them m_waiterLock.Set(); // Bad performance hit here } public void Dispose() { m_waiterLock.Dispose(); } }

Bu qulf SimpleHybridLock ga qaraganda ko'proq maydonlarga ega va mantiqiy murakkab. Keling, Enter metodining ishlashini qadam-baqadam ko'rib chiqamiz:

  1. Rekursiya tekshiruvi: Birinchi navbatda, chaqiruvchi threadning ID si qulfni egallaydigan thread ID si bilan taqqoslanadi. Agar mos kelsa, rekursiya hisoblagichi oshiriladi va darhol qaytiladi.
  2. Spinning: Thread m_spincount marta aylanib, qulf bo'shashini kutadi. Har bir aylanishda Interlocked.CompareExchange bilan qulfni egallashga harakat qiladi.
  3. Kernel-mode kutish: Agar spinning tugagach ham qulf olinmasa, Interlocked.Increment va WaitOne orqali kernel rejimida kutiladi.

Unumdorlik taqqoslash

Quyida turli qulflarning unumdorlik taqqoslash natijalari keltirilgan (tezdan sekinroqqa):

Qulf turiVaqt (x oshirish)Natija
Incrementing x: 8Eng tez (qulfsiz)
Monitor (M): 69~9xSekinroq
SpinLock: 164~21xSekinroq
SimpleHybridLock: 164~21xSpinLock ga o'xshash
AnotherHybridLock: 230~29xEgalik/rekursiya tufayli
SimpleWaitLock: 8,854~1,107xEng sekin

Ko'rib turganingizdek, AnotherHybridLock SimpleHybridLock ga nisbatan sekinroq ishlaydi. Bunga sabab — thread egaligi va rekursiyani boshqarish uchun qo'shimcha mantiq va tekshiruvlar kerak. Har bir qulfga qo'shilgan har bir xatti-harakat uning unumdorligiga ta'sir qiladi.

Framework Class Library (FCL) dagi Gibrid Konstruktsiyalar

FCL ko'plab gibrid konstruktsiyalarni taqdim etadi. Bu konstruktsiyalar murakkab mantiqdan foydalanib, threadlaringizni user rejimida ushlab turishga harakat qiladi va shu bilan ilovangiz unumdorligini oshiradi. Ba'zi gibrid konstruktsiyalar kernel-mode konstruktsiyasini birinchi marta raqobat paydo bo'lgunga qadar yaratmasdan turadi. Agar threadlar hech qachon raqobatlashmasa, ilovangiz obyektni yaratish va xotira ajratish unumdorlik zarbasini oldini oladi.

Ko'plab konstruktsiyalar CancellationToken ni ham qo'llab-quvvatlaydi (27-bobda muhokama qilingan), bu thread kutish jarayonida tashqaridan bekor qilish imkonini beradi.

ManualResetEventSlim va SemaphoreSlim Klasslari

Birinchi ikki gibrid konstruktsiya — System.Threading.ManualResetEventSlim va System.Threading.SemaphoreSlim. Bu konstruktsiyalar o'zlarining kernel-mode ekvivalentlariga aynan o'xshab ishlaydi, farqi shundaki:

  • Ikkalasi ham user rejimida spinning ishlatadi
  • Ikkalasi ham kernel-mode konstruktsiyasini birinchi raqobatgacha kechiktiradi (defer)
  • Wait metodlari timeout va CancellationToken ni qabul qiladi
C# public class ManualResetEventSlim : IDisposable { public ManualResetEventSlim(Boolean initialState, Int32 spinCount); public void Dispose(); public void Reset(); public void Set(); public Boolean Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken); public Boolean IsSet { get; } public Int32 SpinCount { get; } public WaitHandle WaitHandle { get; } }
C# public class SemaphoreSlim : IDisposable { public SemaphoreSlim(Int32 initialCount, Int32 maxCount); public void Dispose(); public Int32 Release(Int32 releaseCount); public Boolean Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken); // Special method for use with async and await (see Chapter 28) public Task<Boolean> WaitAsync(Int32 millisecondsTimeout, CancellationToken cancellationToken); public Int32 CurrentCount { get; } public WaitHandle AvailableWaitHandle { get; } }
Eslatma

AutoResetEventSlim klassi mavjud emas, lekin ko'p holatlarda SemaphoreSlim ni maxCount 1 qilib yaratib, xuddi shu xatti-harakatni olishingiz mumkin.

SemaphoreSlim haqida qo'shimcha

SemaphoreSlim klassining eng muhim xususiyatlaridan biri — WaitAsync metodi mavjudligi. Bu metod async/await bilan birga ishlatilishi mumkin va threadni bloklamasdan asinxron kutish imkonini beradi. Bu haqida bob oxiridagi "Asinxron Sinxronizatsiya" bo'limida batafsil gaplashamiz.

Monitor Klassi va Sync Bloklari

Ehtimol eng ko'p ishlatiladigan gibrid thread sinxronizatsiya konstruktsiyasi — Monitor klassi. U o'zaro eksklyuziv (mutual-exclusive) qulfni ta'minlaydi va spinning, thread egaligi va rekursiyani qo'llab-quvvatlaydi. Bu eng ko'p ishlatiladigan konstruktsiya, chunki:

  • U eng qadimgi konstruktsiya
  • C# tilida uni qo'llab-quvvatlovchi lock kalit so'zi mavjud
  • JIT kompilyator u haqida biladi va uni optimallashtiradi
  • CLR o'zi ham uni ilovangiz nomidan ishlatadi

Biroq, ko'rib turganimizdek, bu konstruktsiya bilan ko'plab muammolar mavjud bo'lib, xatoli kod yozishni osonlashtiradi.

Sync Bloklari qanday ishlaydi

Heapdagi har bir obyekt bilan sync block deb ataladigan ma'lumotlar tuzilmasi bog'lanishi mumkin. Sync block AnotherHybridLock klassiga o'xshash maydonlarni o'z ichiga oladi: kernel obyekti, egalik qiluvchi thread ID, rekursiya hisoblagichi va kutayotgan threadlar soni.

Monitor klassi static klass bo'lib, uning metodlari istalgan heap obyektiga havola qabul qiladi:

C# public static class Monitor { public static void Enter(Object obj); public static void Exit(Object obj); // You can also specify a timeout when entered the lock (not commonly used): public static Boolean TryEnter(Object obj, Int32 millisecondsTimeout); // I'll discuss the lockTaken argument later public static void Enter(Object obj, ref Boolean lockTaken); public static void TryEnter(Object obj, Int32 millisecondsTimeout, ref Boolean lockTaken); }

Har bir obyektga sync block bog'lash xotirani isrof qiladi, chunki ko'pchilik obyektlarning sync bloklari hech qachon ishlatilmaydi. Xotira sarfini kamaytirish uchun CLR samaraliroq yondashuvdan foydalanadi:

  • CLR ishga tushganda, native heapda sync bloklari massivini ajratadi
  • Har bir obyekt yaratilganda, u ikki qo'shimcha overhead maydonga ega bo'ladi: type object pointer va sync block index
  • Dastlab, obyektning sync block indeksi -1 ga o'rnatiladi (hech qanday sync block bilan bog'lanmagan)
  • Monitor.Enter chaqirilganda, CLR massivda bo'sh sync blockni topadi va obyektning sync block indeksini shu blokka yo'naltiradi
  • Monitor.Exit chaqirilganda, agar boshqa kutayotgan threadlar bo'lmasa, sync block indeksi qaytadan -1 ga o'rnatiladi
Managed Heap

Object-A
  Type object ptr
  Sync block index (0) →
  Instance Fields

Object-B
  Type object ptr
  Sync block index (-1)
  Instance Fields

Object-C
  Type object ptr
  Sync block index (2) →
  Instance Fields
CLR's Array of Sync Blocks

Sync block #0
Sync block #1
Sync block #2
Sync block #3
...

30-1 rasm: Obyektlar va ularning sync block indekslari

Monitor klassidan foydalanish (dastlabki usul)

C# internal sealed class Transaction { private DateTime m_timeOfLastTrans; public void PerformTransaction() { Monitor.Enter(this); // This code has exclusive access to the data... m_timeOfLastTrans = DateTime.Now; Monitor.Exit(this); } public DateTime LastTransaction { get { Monitor.Enter(this); // This code has exclusive access to the data... DateTime temp = m_timeOfLastTrans; Monitor.Exit(this); return temp; } } }

Monitor bilan bog'liq muammolar

Yuqoridagi kod sirtdan oddiy ko'rinadi, lekin unda jiddiy muammo bor: har bir obyektning sync block indeksi ommaviy (public) hisoblanadi. Quyidagi kod bu muammoni ko'rsatadi:

C# public static void SomeMethod() { var t = new Transaction(); Monitor.Enter(t); // This thread takes the object's public lock // Have a thread pool thread display the LastTransaction time // NOTE: The thread pool thread blocks until SomeMethod calls Monitor.Exit! ThreadPool.QueueUserWorkItem(o => Console.WriteLine(t.LastTransaction)); // Execute some other code here... Monitor.Exit(t); }

Bu kodda SomeMethod threadning Monitor.Enter chaqiruvi Transaction obyektining ommaviy qulfini oladi. Thread pool threadning LastTransaction xususiyatiga murojaat qilishi xuddi shu qulfni olishga harakat qiladi — natijada thread pool thread SomeMethod Monitor.Exit chaqirguncha bloklanadi.

Yechim: Har doim xususiy (private) qulfdan foydalaning!

this ga qulf qo'yish o'rniga, har doim xususiy obyekt yaratib, unga qulf qo'ying.

C# internal sealed class Transaction { private readonly Object m_lock = new Object(); // Each transaction has a PRIVATE lock now private DateTime m_timeOfLastTrans; public void PerformTransaction() { Monitor.Enter(m_lock); // Enter the private lock // This code has exclusive access to the data... m_timeOfLastTrans = DateTime.Now; Monitor.Exit(m_lock); // Exit the private lock } public DateTime LastTransaction { get { Monitor.Enter(m_lock); // Enter the private lock // This code has exclusive access to the data... DateTime temp = m_timeOfLastTrans; Monitor.Exit(m_lock); // Exit the private lock return temp; } } }

Monitor static klass sifatida amalga oshirilmasligi kerak edi; u oddiy klass sifatida amalga oshirilishi, instance yaratilishi va instance metodlari chaqirilishi kerak edi. Aslida, Monitor ning static klass bo'lishi bilan bog'liq ko'plab qo'shimcha muammolar ham mavjud:

  • Proksi obyektlar: MarshalByRefObject dan hosil bo'lgan turga havola o'tkazilganda, siz haqiqiy obyekt emas, balki proksi obyektni qulflamoqdasiz.
  • Domain-neytral turlar: Turga havola Monitor.Enter ga o'tkazilganda, shu turga barcha AppDomainlarda qulf qo'yiladi. Bu CLR dagi ma'lum xato.
  • Interned stringlar: Stringlar intern qilinishi mumkin (14-bob), shuning uchun ikki alohida kodning bir xil String obyektiga havola qilishi va bir-birini bilmasdan sinxronizatsiya qilishi mumkin.
  • Value turlar: Monitor metodlari Object qabul qilganligi sababli, value turni o'tkazish boxing ga olib keladi. Har safar yangi boxed obyekt yaratiladi va Monitor.Enter har safar boshqa obyektga qulf qo'yadi — sinxronizatsiya umuman ishlamaydi!
  • [MethodImpl(MethodImplOptions.Synchronized)]: Bu atributni hech qachon ishlatmang. Instance metod bo'lsa this ga, statik metod bo'lsa tur obyektiga qulf qo'yadi.
  • Tur konstruktorlari: CLR tur konstruktorlarida tur obyektiga qulf qo'yadi. Agar tur domain-neytral yuklangan bo'lsa, muammo kelib chiqadi.

C# ning lock Kalit So'zi

Qulfni olish, biror ish qilish va qulfni bo'shatish juda keng tarqalgan amaliyot bo'lganligi sababli, C# tili soddalashtirilgan sintaksisni taklif etadi — lock kalit so'zi:

C# private void SomeMethod() { lock (this) { // This code has exclusive access to the data... } }

Bu quyidagi kodga ekvivalent:

C# private void SomeMethod() { Boolean lockTaken = false; try { // An exception (such as ThreadAbortException) could occur here... Monitor.Enter(this, ref lockTaken); // This code has exclusive access to the data... } finally { if (lockTaken) Monitor.Exit(this); } }
lock ishlatmaslik tavsiya etiladi!

Muallif lock kalit so'zini ishlatmaslikni maslahat beradi. Buning bir nechta sababi bor:

  • finally blokidagi Exit: C# jamoasi qulfni finally blokida bo'shatish yaxshi deb hisobladi. Lekin agar try bloki ichida istisno bo'lsa, holat buzilgan bo'lishi mumkin va finally blokida qulfni bo'shatish boshqa threadga buzilgan holatga kirishga imkon beradi. Ilovaning osib qolishi (hang) xavfsiz emas holat bilan ishlashdan yaxshiroq.
  • Unumdorlik: try/finally blokiga kirish va chiqish unumdorlikni pasaytiradi. Ba'zi JIT kompilyatorlari try bloki bo'lgan metodlarni inline qila olmaydi.

lockTaken parametri

Boolean lockTaken o'zgaruvchisi quyidagi muammoni hal qiladi: agar thread try blokiga kiradi va Monitor.Enter chaqirishdan oldin abort qilinsa, finally bloki chaqiriladi, lekin qulf olinmagan. lockTaken false bilan boshlandi va faqat Monitor.Enter muvaffaqiyatli bo'lsa true ga o'zgaradi. finally bloki lockTaken ni tekshirib, Monitor.Exit ni faqat qulf olingan bo'lsa chaqiradi. SpinLock strukturasi ham bu patternni qo'llab-quvvatlaydi.

ReaderWriterLockSlim Klassi

Threadlarning ma'lumotlarni oddiygina o'qishi juda keng tarqalgan. Agar bu ma'lumotlar o'zaro eksklyuziv qulf (SimpleSpinLock, Monitor va boshqalar) bilan himoyalangan bo'lsa, bir nechta threadlar bir vaqtda kirishga harakat qilganda faqat bitta thread ishlaydi, qolganlari bloklanadi — bu scalability va throughput ni sezilarli kamaytiradi.

Agar barcha threadlar ma'lumotlarni faqat o'qimoqchi bo'lsa, ularni umuman bloklashga hojat yo'q. Boshqa tomondan, agar biror thread ma'lumotlarni o'zgartirmoqchi bo'lsa, u eksklyuziv kirish huquqiga muhtoj. ReaderWriterLockSlim aynan shu mantiqni amalga oshiradi:

  • Biror thread yozayotganda, boshqa barcha threadlar (o'qish va yozish so'ragan) bloklanadi
  • Biror thread o'qiyotganda, boshqa o'qish so'ragan threadlar ham o'qiy oladi, lekin yozish so'ragan threadlar bloklanadi
  • Yozish tugaganda, bitta yozuvchi thread yoki barcha o'quvchi threadlar blokdan chiqariladi
  • Barcha o'quvchilar o'qib bo'lganda, bitta yozuvchi thread blokdan chiqariladi
C# public class ReaderWriterLockSlim : IDisposable { public ReaderWriterLockSlim(LockRecursionPolicy recursionPolicy); public void Dispose(); public void EnterReadLock(); public Boolean TryEnterReadLock(Int32 millisecondsTimeout); public void ExitReadLock(); public void EnterWriteLock(); public Boolean TryEnterWriteLock(Int32 millisecondsTimeout); public void ExitWriteLock(); // Most applications will never query any of these properties public Boolean IsReadLockHeld { get; } public Boolean IsWriteLockHeld { get; } public Int32 CurrentReadCount { get; } public Int32 RecursiveReadCount { get; } public Int32 RecursiveWriteCount { get; } public Int32 WaitingReadCount { get; } public Int32 WaitingWriteCount { get; } public LockRecursionPolicy RecursionPolicy { get; } // Members related to upgrading from a reader to a writer not shown }

Mana bu konstruktsiyani ishlatish namunasi:

C# internal sealed class Transaction : IDisposable { private readonly ReaderWriterLockSlim m_lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); private DateTime m_timeOfLastTrans; public void PerformTransaction() { m_lock.EnterWriteLock(); // This code has exclusive access to the data... m_timeOfLastTrans = DateTime.Now; m_lock.ExitWriteLock(); } public DateTime LastTransaction { get { m_lock.EnterReadLock(); // This code has shared access to the data... DateTime temp = m_timeOfLastTrans; m_lock.ExitReadLock(); return temp; } } public void Dispose() { m_lock.Dispose(); } }
LockRecursionPolicy haqida

ReaderWriterLockSlim konstruktori LockRecursionPolicy bayrog'ini qabul qiladi:

  • NoRecursion — thread egaligi va rekursiyani o'chiradi (tavsiya etiladi)
  • SupportsRecursion — thread egaligi va rekursiyani yoqadi (unumdorlikni pasaytiradi)

Reader-writer qulf uchun rekursiyani qo'llab-quvvatlash juda qimmat, chunki qulf qo'yib yuborgan barcha o'quvchi threadlarni kuzatishi va har biri uchun alohida rekursiya hisoblagichini yuritishi kerak. Aslida, ReaderWriterLockSlim ichki ravishda o'zaro eksklyuziv spinlock ishlatadi! Har doim NoRecursion dan foydalanish tavsiya etiladi.

Eslatma

FCL shuningdek eski ReaderWriterLock konstruktsiyasini ham o'z ichiga oladi (NET Framework 1.0 dan beri). Bu konstruktsiyada ko'plab muammolar mavjud edi, shu sababli ReaderWriterLockSlim .NET Framework 3.5 da joriy qilindi. Eski konstruktsiyaning muammolari: thread raqobatisiz ham juda sekin, thread egaligi va rekursiyani o'chirish imkoni yo'q, o'quvchi threadlarni yozuvchilardan ustun qo'yadi — natijada yozuvchilar navbatga turib, kamdan-kam xizmat ko'radi (denial of service muammolari).

OneManyLock Klassi

Jeffrey Richter FCL ning ReaderWriterLockSlim dan tezroq ishlaydigan o'z reader-writer konstruktsiyasini yaratgan. U buni OneManyLock deb atagan, chunki u bitta yozuvchi thread yoki ko'p o'quvchi threadlarga kirish huquqini beradi:

C# public sealed class OneManyLock : IDisposable { public OneManyLock(); public void Dispose(); public void Enter(Boolean exclusive); public void Leave(); }

Ichki ishlashi: klassda Int32 maydoni, o'quvchi threadlar bloklanadigon Semaphore va yozuvchi threadlar bloklanadigon AutoResetEvent mavjud. Int64 holat maydoni beshta sub-maydonga bo'lingan:

  • 4 bit: Qulf holati (0=Free, 1=OwnedByWriter, 2=OwnedByReaders, 3=OwnedByReadersAndWriterPending, 4=ReservedForWriter)
  • 20 bit: Hozirda o'qiyotgan threadlar soni (RR — Reader Reading)
  • 20 bit: Kutayotgan o'quvchilar soni (RW — Readers Waiting)
  • 20 bit: Kutayotgan yozuvchilar soni (WW — Writers Waiting)

Barcha ma'lumotlar bitta Int64 maydoniga sig'ganligi sababli, Interlocked klassining metodlari bilan bu maydonni atomik tarzda boshqarish mumkin. Bu qulfni juda tez va faqat raqobat bo'lganda threadlarni bloklaydi.

Unumdorlik taqqoslash (reader-writer qulflar)

Qulf turiVaqtTaqqoslash
OneManyLock: 330Eng tez
ReaderWriterLockSlim: 554~1.7xSekinroq
ReaderWriterLock: 984~3xEng sekin

Albatta, barcha reader-writer qulflar o'zaro eksklyuziv qulfdan ko'ra ko'proq mantiq bajarishi kerak, shuning uchun ularning unumdorligi biroz pastroq bo'lishi mumkin. Lekin reader-writer qulf bir nechta o'quvchilarni bir vaqtda kiritish imkonini beradi — bu esa scalability uchun juda muhim.

CountdownEvent Klassi

Keyingi konstruktsiya — System.Threading.CountdownEvent. Ichki ravishda u ManualResetEventSlim obyektidan foydalanadi. Bu konstruktsiya ichki hisoblagich 0 ga yetgunga qadar threadni bloklaydi. Bu Semaphore ning teskari xatti-harakati — Semaphore hisoblagichi 0 bo'lganda threadlarni bloklaydi, CountdownEvent esa hisoblagichi 0 bo'lganda threadlarni blokdan chiqaradi.

C# public class CountdownEvent : IDisposable { public CountdownEvent(Int32 initialCount); public void Dispose(); public void Reset(Int32 count); // Set CurrentCount to count public void AddCount(Int32 signalCount); // Increments CurrentCount by signalCount public Boolean TryAddCount(Int32 signalCount); // Increments CurrentCount by signalCount public Boolean Signal(Int32 signalCount); // Decrements CurrentCount by signalCount public Boolean Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken); public Int32 CurrentCount { get; } public Boolean IsSet { get; } // true if CurrentCount is 0 public WaitHandle WaitHandle { get; } }

CountdownEvent ning CurrentCount 0 ga yetganda, uni o'zgartirish mumkin emas. AddCount metodi CurrentCount 0 bo'lganda InvalidOperationException tashlaydi, TryAddCount esa false qaytaradi.

Barrier Klassi

System.Threading.Barrier konstruktsiyasi juda noyob muammoni hal qilish uchun mo'ljallangan — parallel ishlaydigan threadlar guruhini algoritmning bosqichlari (fazalari) bo'ylab sinxron qilib olib borish. Masalan, CLR ning server GC (garbage collector) algoritmi buning yaxshi namunasi: u har bir yadro uchun bitta thread yaratadi va bu threadlar heapni bir vaqtda belgilab (mark) chiqadi. Har bir thread o'z qismini tugatganda, boshqa threadlarni kutishi kerak. Barcha threadlar tugatgandan so'ng, heapni siqish (compact) bosqichi boshlanadi.

C# public class Barrier : IDisposable { public Barrier(Int32 participantCount, Action<Barrier> postPhaseAction); public void Dispose(); public Int64 AddParticipants(Int32 participantCount); // Adds participants public void RemoveParticipants(Int32 participantCount); // Subtracts participants public Boolean SignalAndWait(Int32 millisecondsTimeout, CancellationToken cancellationToken); public Int64 CurrentPhaseNumber { get; } // Indicates phase in process (starts at 0) public Int32 ParticipantCount { get; } // Number of participants public Int32 ParticipantsRemaining { get; } // # of threads needing to call // SignalAndWait }

Barrier yaratganingizda, ishda nechta thread ishtirok etishini belgilaysiz va har bir fazani tugallanganda chaqiriladigan Action<Barrier> delegatini o'tkazishingiz mumkin. Har bir ishtirokchi thread o'z fazasini tugatganda SignalAndWait ni chaqirishi kerak — bu Barrier ga ish tugatilganini bildiradi va threadni bloklaydi (ManualResetEventSlim orqali). Barcha ishtirokchilar SignalAndWait ni chaqirgandan so'ng, Barrier delegatni chaqiradi va barcha kutayotgan threadlarni blokdan chiqaradi.

Thread Sinxronizatsiya Konstruktsiyalari Xulosasi

Muallifning asosiy tavsiyasi — threadlarni bloklaydigan kod yozishdan iloji boricha qoching. Asinxron hisoblash yoki I/O operatsiyalarni bajarganingizda, ma'lumotlarni threaddan threadga shunday uzatishga harakat qilingki, bir nechta threadning bir vaqtda ma'lumotlarga kirishiga yo'l qo'ymang.

Agar bunga to'liq erisha olmasangiz, Volatile va Interlocked metodlaridan foydalanishga harakat qiling — ular tez va threadni hech qachon bloklamaydi. Lekin bu metodlar faqat oddiy turlarni boshqaradi.

Threadlarni bloklashning ikkita asosiy sababi bor:

  • Dasturlash modeli soddalashadi: Threadni bloklash orqali ilovangiz kodini callback metodlardan foydalanmasdan ketma-ket yozishingiz mumkin. Lekin C# ning async metod xususiyati threadlarni bloklamasdan ham soddalashtirilgan dasturlash modelini beradi.
  • Threadning maxsus vazifasi bor: Ba'zi threadlar maxsus vazifalar uchun mo'ljallangan. Masalan, ilova asosiy threadi bloklanmasa, jarayon tugaydi. Boshqa misol — GUI threadi.

Threadlarni bloklashdan qochish uchun, threadlaringizga aqliy yorliq qo'ymang. Thread pool dan foydalanib, qisqa muddatli ishlarni bajarish uchun threadlarni "ijaraga" oling.

Agar threadlarni bloklashga qaror qilsangiz, quyidagi tavsiyalarga amal qiling:

  • Turli AppDomainlar yoki jarayonlardagi threadlarni sinxronlashtirish uchun kernel obyekt konstruktsiyalaridan foydalaning
  • Holatni atomik ravishda boshqarish uchun Monitor klassini xususiy (private) maydon bilan ishlating
  • Ko'p o'quvchilar va kam yozuvchilar bo'lsa, Monitor o'rniga reader-writer qulfdan foydalaning — bu ko'p o'quvchilarni bir vaqtda ishlashga imkon beradi
  • Rekursiv qulflardan qoching — ular unumdorlikni pasaytiradi
  • Qulfni finally blokida bo'shatishdan saqlaning — bu unumdorlik zarba va buzilgan holatda xavfsizlik teshiklari
  • Qulfni iloji boricha qisqa vaqt ushlab turing
  • Hisoblash ishlari uchun Task (27-bob) dan foydalanib, ko'p sinxronizatsiya konstruktsiyalaridan qoching

Mashhur Double-Check Locking Texnikasi

Double-check locking — dasturchilarga singleton obyektni ilova so'ragunga qadar kechiktirib yaratish imkonini beradigan mashhur texnika (ba'zan lazy initialization deyiladi). Agar ilova obyektni hech qachon so'ramasa, u hech qachon yaratilmaydi — vaqt va xotira tejaladi.

Potentsial muammo: bir nechta thread bir vaqtda singleton obyektni so'rashi mumkin. Bunday holda, obyektning faqat bitta marta yaratilishini ta'minlash uchun thread sinxronizatsiyasi kerak.

Java dagi muammo

Bu texnika Java da keng qo'llanilgan, lekin keyinchalik Java ning xotira modeli bu texnikaning to'g'ri ishlashini kafolatlamasligi aniqlandi. CLR esa o'zining xotira modeli va volatile maydon kirishi tufayli bu texnikani to'g'ri qo'llab-quvvatlaydi.

Quyida C# da double-check locking texnikasini amalga oshirish ko'rsatilgan:

C# internal sealed class Singleton { // s_lock is required for thread safety and having this object assumes that creating // the singleton object is more expensive than creating a System.Object object and that // creating the singleton object may not be necessary at all. Otherwise, it is more // efficient and easier to just create the singleton object in a class constructor private static readonly Object s_lock = new Object(); // This field will refer to the one Singleton object private static Singleton s_value = null; // Private constructor prevents any code outside this class from creating an instance private Singleton() { // Code to initialize the one Singleton object goes here... } // Public, static method that returns the Singleton object (creating it if necessary) public static Singleton GetSingleton() { // If the Singleton was already created, just return it (this is fast) if (s_value != null) return s_value; Monitor.Enter(s_lock); // Not created, let 1 thread create it if (s_value == null) { // Still not created, create it Singleton temp = new Singleton(); // Save the reference in s_value (see discussion for details) Volatile.Write(ref s_value, temp); } Monitor.Exit(s_lock); // Return a reference to the one Singleton object return s_value; } }

Qanday ishlaydi

GetSingleton metodi birinchi navbatda s_value maydonini tekshiradi — agar obyekt allaqachon yaratilgan bo'lsa, havola qaytariladi. Bu juda tez, chunki thread sinxronizatsiyasi talab qilinmaydi.

Agar birinchi thread GetSingleton ni chaqirganda obyekt yaratilmagan bo'lsa, qulfni oladi va faqat bitta thread obyektni yaratishini ta'minlaydi. Unumdorlik zarbasi faqat birinchi marta thread singleton obyektni so'raganda bo'ladi.

Volatile.Write nima uchun kerak?

Kompilyator quyidagi kodni ishlab chiqarishini kutishingiz mumkin:

  1. Singleton uchun xotira ajratish
  2. Konstruktorni chaqirish
  3. Havolani s_value ga yozish

Lekin kompilyator buning o'rniga shunday qilishi mumkin:

  1. Singleton uchun xotira ajratish
  2. Havolani s_value ga yozish (publish)
  3. Konstruktorni chaqirish

Bitta threadning nuqtai nazaridan tartib farq qilmaydi. Lekin agar 2-qadam va 3-qadam o'rtasida boshqa thread GetSingleton ni chaqirsa, u s_value null emasligini ko'radi va konstruktori hali tugallanmagan obyektni ishlatishni boshlaydi! Bu juda qiyin aniqlanadigan xato.

Volatile.Write chaqiruvi temp dagi havolaning s_value ga faqat konstruktor tugallangandan keyin yozilishini kafolatlaydi.

Soddaroq alternativa

Muallif double-check locking texnikasining ko'pincha ortiqcha ishlatilishini ta'kidlaydi. Ko'p holatlarda ancha sodda yondashuv ham ishlaydi:

C# internal sealed class Singleton { private static Singleton s_value = new Singleton(); // Private constructor prevents any code outside this class from creating an instance private Singleton() { // Code to initialize the one Singleton object goes here... } // Public, static method that returns the Singleton object public static Singleton GetSingleton() { return s_value; } }

CLR avtomatik ravishda tur konstruktorini chaqiradi va tur konstruktorlari thread-xavfsiz bo'lishini kafolatlaydi. Kamchilik shundaki, agar Singleton turida boshqa statik a'zolar bo'lsa, ulardan biriga birinchi kirish ham singleton yaratilishiga olib keladi.

Interlocked.CompareExchange bilan alternativa

C# internal sealed class Singleton { private static Singleton s_value = null; // Private constructor prevents any code outside this class from creating an instance private Singleton() { // Code to initialize the one Singleton object goes here... } // Public, static method that returns the Singleton object (creating it if necessary) public static Singleton GetSingleton() { if (s_value != null) return s_value; // Create a new Singleton and root it if another thread didn't do it first Singleton temp = new Singleton(); Interlocked.CompareExchange(ref s_value, temp, null); // If this thread lost, then the second Singleton object gets GC'd return s_value; // Return reference to the single object } }

Agar bir nechta thread bir vaqtda GetSingleton chaqirsa, bu versiyada ikki (yoki undan ortiq) Singleton yaratilishi mumkin. Lekin Interlocked.CompareExchange faqat bitta havolani s_value maydoniga joylashtiradi. Qolgan obyektlar garbage collect qilinadi.

Bu yondashuvning ko'p afzalliklari bor: juda tez, hech qachon threadni bloklamaydi, va agar thread pool thread Monitor yoki boshqa kernel-mode konstruktsiyada bloklangan bo'lsa, thread pool boshqa thread yaratib CPU larni ishlatishda davom etadi. Albatta, bu texnikani faqat konstruktorda yon ta'sir (side effects) bo'lmaganda ishlatish mumkin.

Lazy<T> va LazyInitializer

FCL yuqoridagi patternlarni inkapsulatsiya qiluvchi ikki turni taklif etadi. Generik System.Lazy<T> klassi:

C# public class Lazy<T> { public Lazy<T>(Func<T> valueFactory, LazyThreadSafetyMode mode); public Boolean IsValueCreated { get; } public T Value { get; } }
C# public static void Main() { // Create a lazy-initialization wrapper around getting the DateTime Lazy<String> s = new Lazy<String>(() => DateTime.Now.ToLongTimeString(), true); Console.WriteLine(s.IsValueCreated); // Returns false because Value not queried yet Console.WriteLine(s.Value); // The delegate is invoked now Console.WriteLine(s.IsValueCreated); // Returns true because Value was queried Thread.Sleep(10000); // Wait 10 seconds and display the time again Console.WriteLine(s.Value); // The delegate is NOT invoked now; same result }

Natija:

False 2:40:42 PM True 2:40:42 PM & Notice that the time did not change 10 seconds later

LazyThreadSafetyMode bayroqlari:

C# public enum LazyThreadSafetyMode { None, // No thread-safety support at all (good for GUI apps) ExecutionAndPublication, // Uses the double-check locking technique PublicationOnly, // Uses the Interlocked.CompareExchange technique }

Xotira cheklangan stsenariylarda Lazy<T> misolini yaratish o'rniga, System.Threading.LazyInitializer klassining statik metodlaridan foydalanishingiz mumkin:

C# public static class LazyInitializer { // These two methods use Interlocked.CompareExchange internally: public static T EnsureInitialized<T>(ref T target) where T : class; public static T EnsureInitialized<T>(ref T target, Func<T> valueFactory) where T : class; // These two methods pass the syncLock to Monitor's Enter and Exit methods internally public static T EnsureInitialized<T>(ref T target, ref Boolean initialized, ref Object syncLock); public static T EnsureInitialized<T>(ref T target, ref Boolean initialized, ref Object syncLock, Func<T> valueFactory); }
C# public static void Main() { String name = null; // Because name is null, the delegate runs and initializes name LazyInitializer.EnsureInitialized(ref name, () => "Jeffrey"); Console.WriteLine(name); // Displays "Jeffrey" // Because name is not null, the delegate does not run; name doesn't change LazyInitializer.EnsureInitialized(ref name, () => "Richter"); Console.WriteLine(name); // Also displays "Jeffrey" }

Condition Variable Pattern (Shart O'zgaruvchisi Patterni)

Aytaylik, thread murakkab shart bajarilganda ba'zi kodni bajarishni xohlaydi. Bitta variant — threadni uzluksiz spinning qildirib, shartni qayta-qayta tekshirish. Lekin bu CPU vaqtini isrof qiladi va bir nechta o'zgaruvchilarni atomik tarzda tekshirish mumkin emas.

Yaxshiyamki, condition variable pattern (shart o'zgaruvchisi patterni) deb ataladigan pattern mavjud. Biz uni Monitor klassi ichida aniqlangan quyidagi metodlar orqali ishlatamiz:

C# public static class Monitor { public static Boolean Wait(Object obj); public static Boolean Wait(Object obj, Int32 millisecondsTimeout); public static void Pulse(Object obj); public static void PulseAll(Object obj); }

Mana bu patternning ko'rinishi:

C# internal sealed class ConditionVariablePattern { private readonly Object m_lock = new Object(); private Boolean m_condition = false; public void Thread1() { Monitor.Enter(m_lock); // Acquire a mutual-exclusive lock // While under the lock, test the complex condition "atomically" while (!m_condition) { // If condition is not met, wait for another thread to change the condition Monitor.Wait(m_lock); // Temporarily release lock so other threads can get it } // The condition was met, process the data... Monitor.Exit(m_lock); // Permanently release lock } public void Thread2() { Monitor.Enter(m_lock); // Acquire a mutual-exclusive lock // Process data and modify the condition... m_condition = true; Monitor.Pulse(m_lock); // Wakes one waiter AFTER lock is released Monitor.PulseAll(m_lock); // Wakes all waiters AFTER lock is released Monitor.Exit(m_lock); // Release lock } }

Qanday ishlaydi

Thread1 ni bajarayotgan thread o'zaro eksklyuziv qulfni oladi va shartni tekshiradi. Shart bajarilmagan bo'lsa, Monitor.Wait chaqiradi — bu qulfni vaqtincha bo'shatadi va threadni bloklaydi, boshqa threadlar qulfni olish imkoniga ega bo'ladi.

Thread2 ni bajarayotgan thread qulfni oladi, ma'lumotlarni qayta ishlaydi, shartni o'zgartiradi va Pulse (bitta kutayotgan threadni uyg'otadi) yoki PulseAll (barcha kutayotgan threadlarni uyg'otadi) chaqiradi. Thread2 Monitor.Exit chaqirganda, uyg'otilgan thread qulfni qaytadan oladi.

Uyg'ongan Thread1 yana loop da shartni tekshiradi. Agar shart hali ham bajarilmagan bo'lsa, Monitor.Wait ni yana chaqiradi. Agar shart bajarilgan bo'lsa, ma'lumotlarni qayta ishlaydi va qulfni bo'shatadi.

Thread-xavfsiz navbat (queue) namunasi

C# internal sealed class SynchronizedQueue<T> { private readonly Object m_lock = new Object(); private readonly Queue<T> m_queue = new Queue<T>(); public void Enqueue(T item) { Monitor.Enter(m_lock); // After enqueuing an item, wake up any/all waiters m_queue.Enqueue(item); Monitor.PulseAll(m_lock); Monitor.Exit(m_lock); } public T Dequeue() { Monitor.Enter(m_lock); // Loop while the queue is empty (the condition) while (m_queue.Count == 0) Monitor.Wait(m_lock); // Dequeue an item from the queue and return it for processing T item = m_queue.Dequeue(); Monitor.Exit(m_lock); return item; } }

Bu misolda Dequeue metodini chaqirgan threadlar navbat bo'sh bo'lsa bloklanadi. Enqueue metodi element qo'shgandan so'ng PulseAll chaqirib, barcha kutayotgan threadlarni uyg'otadi. Bu patternning chiroyli tomoni shundaki, bitta qulf yordamida bir nechta o'zgaruvchidan iborat murakkab shartni tekshirish mumkin.

Asinxron Sinxronizatsiya

Muallif kernel-mode primitivlaridan foydalanadigan barcha thread sinxronizatsiya konstruktsiyalarini yoqtirmaydi, chunki bu primitivlarning barchasi threadni bloklash uchun mavjud — bu esa threadlarni yaratish va ishlatmaslikning qimmatligi bilan ziddiyatli.

Misol: veb-saytga kelgan so'rovlarni qayta ishlash. Yozuvchi thread reader-writer qulfni oladi. Bu vaqtda boshqa so'rovlar keladi — thread pool har biri uchun yangi thread yaratadi, lekin ularning barchasi o'qish qulfini olishga harakat qilib bloklanadi. Server barcha vaqtini thread yaratish va ularni to'xtatishga sarflaydi — bu umuman scalable emas!

Kerakli narsa — asinxron sinxronizatsiya konstruktsiyalari: kod qulfni xohlashini bildiradi, agar qulf mavjud bo'lmasa, kod boshqa ishlar bilan shug'ullanib, qulf bo'shaganda davom etadi — threadni hech qachon bloklamasdan.

SemaphoreSlim klassi bu g'oyani WaitAsync metodi orqali amalga oshiradi:

C# public Task<Boolean> WaitAsync(Int32 millisecondsTimeout, CancellationToken cancellationToken);

Bu yordamida resursga asinxron tarzda (hech qanday threadni bloklamasdan) sinxron kirish mumkin:

C# private static async Task AccessResourceViaAsyncSynchronization(SemaphoreSlim asyncLock) { // TODO: Execute whatever code you want here... await asyncLock.WaitAsync(); // Request exclusive access to a resource via its lock // When we get here, we know that no other thread is accessing the resource // TODO: Access the resource (exclusively)... // When done accessing resource, relinquish lock so other code can access the resource asyncLock.Release(); // TODO: Execute whatever code you want here... }

SemaphoreSlim ning WaitAsync metodi juda foydali. Odatda SemaphoreSlim ni maxCount 1 bilan yaratasiz — bu sizga Monitor dan foydalangandek o'zaro eksklyuziv kirish huquqini beradi, farqi shundaki SemaphoreSlim thread egaligi va rekursiyani qo'llab-quvvatlamaydi (bu yaxshi!).

ConcurrentExclusiveSchedulerPair

Reader-writer semantikasi haqida nima deyish mumkin? .NET Framework ConcurrentExclusiveSchedulerPair klassini taklif qiladi:

C# public class ConcurrentExclusiveSchedulerPair { public ConcurrentExclusiveSchedulerPair(); public TaskScheduler ExclusiveScheduler { get; } public TaskScheduler ConcurrentScheduler { get; } // Other methods not shown... }

Bu klassning misolida ikki TaskScheduler obyekti mavjud bo'lib, ular reader/writer semantikasini ta'minlaydi:

  • ExclusiveScheduler orqali rejalashtirilgan vazifalar birma-bir bajariladi, faqat ConcurrentScheduler orqali rejalashtirilgan vazifalar ishlamayotganda
  • ConcurrentScheduler orqali rejalashtirilgan vazifalar bir vaqtda bajarilishi mumkin, faqat ExclusiveScheduler orqali rejalashtirilgan vazifalar ishlamayotganda
C# private static void ConcurrentExclusiveSchedulerDemo() { var cesp = new ConcurrentExclusiveSchedulerPair(); var tfExclusive = new TaskFactory(cesp.ExclusiveScheduler); var tfConcurrent = new TaskFactory(cesp.ConcurrentScheduler); for (Int32 operation = 0; operation < 5; operation++) { var exclusive = operation < 2; // For demo, I make 2 exclusive & 3 concurrent (exclusive ? tfExclusive : tfConcurrent).StartNew(() => { Console.WriteLine("{0} access", exclusive ? "exclusive" : "concurrent"); // TODO: Do exclusive write or concurrent read computation here... }); } }

AsyncOneManyLock

Afsuski, .NET Framework reader-writer semantikasiga ega asinxron qulfni taqdim etmaydi. Shuning uchun muallif o'zi AsyncOneManyLock klassini yaratgan. U SemaphoreSlim kabi ishlatiladi:

C# private static async Task AccessResourceViaAsyncSynchronization(AsyncOneManyLock asyncLock) { // TODO: Execute whatever code you want here... // Pass OneManyMode.Exclusive or OneManyMode.Shared for wanted concurrent access await asyncLock.AcquireAsync(OneManyMode.Shared); // Request shared access // When we get here, no threads are writing to the resource; other threads may be reading // TODO: Read from the resource... // When done accessing resource, relinquish lock so other code can access the resource asyncLock.Release(); // TODO: Execute whatever code you want here... }
C# public enum OneManyMode { Exclusive, Shared } public sealed class AsyncOneManyLock { #region Lock code private SpinLock m_lock = new SpinLock(true); // Don't use readonly with a SpinLock private void Lock() { Boolean taken = false; m_lock.Enter(ref taken); } private void Unlock() { m_lock.Exit(); } #endregion #region Lock state and helper methods private Int32 m_state = 0; private Boolean IsFree { get { return m_state == 0; } } private Boolean IsOwnedByWriter { get { return m_state == -1; } } private Boolean IsOwnedByReaders { get { return m_state > 0; } } private Int32 AddReaders(Int32 count) { return m_state += count; } private Int32 SubtractReader() { return --m_state; } private void MakeWriter() { m_state = -1; } private void MakeFree() { m_state = 0; } #endregion // For the no-contention case to improve performance and reduce memory consumption private readonly Task m_noContentionAccessGranter; // Each waiting writers wakes up via their own TaskCompletionSource queued here private readonly Queue<TaskCompletionSource<Object>> m_qWaitingWriters = new Queue<TaskCompletionSource<Object>>(); // All waiting readers wake up by signaling a single TaskCompletionSource private TaskCompletionSource<Object> m_waitingReadersSignal = new TaskCompletionSource<Object>(); private Int32 m_numWaitingReaders = 0; public AsyncOneManyLock() { m_noContentionAccessGranter = Task.FromResult<Object>(null); } public Task WaitAsync(OneManyMode mode) { Task accressGranter = m_noContentionAccessGranter; // Assume no contention Lock(); switch (mode) { case OneManyMode.Exclusive: if (IsFree) { MakeWriter(); // No contention } else { // Contention: Queue new writer task & return it so writer waits var tcs = new TaskCompletionSource<Object>(); m_qWaitingWriters.Enqueue(tcs); accressGranter = tcs.Task; } break; case OneManyMode.Shared: if (IsFree || (IsOwnedByReaders && m_qWaitingWriters.Count == 0)) { AddReaders(1); // No contention } else { // Contention m_numWaitingReaders++; accressGranter = m_waitingReadersSignal.Task.ContinueWith(t => t.Result); } break; } Unlock(); return accressGranter; } public void Release() { TaskCompletionSource<Object> accessGranter = null; Lock(); if (IsOwnedByWriter) MakeFree(); // The writer left else SubtractReader(); // A reader left if (IsFree) { // If free, wake 1 waiting writer or all waiting readers if (m_qWaitingWriters.Count > 0) { MakeWriter(); accessGranter = m_qWaitingWriters.Dequeue(); } else if (m_numWaitingReaders > 0) { AddReaders(m_numWaitingReaders); m_numWaitingReaders = 0; accessGranter = m_waitingReadersSignal; // Create a new TCS for future readers that need to wait m_waitingReadersSignal = new TaskCompletionSource<Object>(); } } Unlock(); // Wake the writer/reader outside the lock to reduce // chance of contention improving performance if (accessGranter != null) accessGranter.SetResult(null); } }

Bu kod hech qachon threadni bloklamaydi — chunki u hech qanday kernel konstruktsiyalarini ishlatmaydi. Ichki ravishda SpinLock ishlatiladi, lekin u faqat user-mode konstruktsiyasi. WaitAsync metodida qulf ushlab turgan vaqtda faqat butun sonli hisob-kitoblar, taqqoslash va ehtimol TaskCompletionSource yaratish amalga oshiriladi. Bu juda qisqa vaqt, shuning uchun qulf qisqa ushlanadi va threadlar hech qachon bloklanmaydi.

Concurrent (Parallel) To'plam Klasslari

FCL to'rtta thread-xavfsiz to'plam klassini taqdim etadi, barchasi System.Collections.Concurrent nomlar fazosida joylashgan:

C# // Process items in a first-in, first-out order (FIFO) public class ConcurrentQueue<T> : IProducerConsumerCollection<T>, IEnumerable<T>, ICollection, IEnumerable { public ConcurrentQueue(); public void Enqueue(T item); public Boolean TryDequeue(out T result); public Int32 Count { get; } public IEnumerator<T> GetEnumerator(); }
C# // Process items in a last-in, first-out order (LIFO) public class ConcurrentStack<T> : IProducerConsumerCollection<T>, IEnumerable<T>, ICollection, IEnumerable { public ConcurrentStack(); public void Push(T item); public Boolean TryPop(out T result); public Int32 Count { get; } public IEnumerator<T> GetEnumerator(); }
C# // An unordered set of items where duplicates are allowed public class ConcurrentBag<T> : IProducerConsumerCollection<T>, IEnumerable<T>, ICollection, IEnumerable { public ConcurrentBag(); public void Add(T item); public Boolean TryTake(out T result); public Int32 Count { get; } public IEnumerator<T> GetEnumerator(); }
C# // An unordered set of key/value pairs public class ConcurrentDictionary<TKey, TValue> : IDictionary<TKey, TValue>, ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IDictionary, ICollection, IEnumerable { public ConcurrentDictionary(); public Boolean TryAdd(TKey key, TValue value); public Boolean TryGetValue(TKey key, out TValue value); public TValue this[TKey key] { get; set; } public Boolean TryUpdate(TKey key, TValue newValue, TValue comparisonValue); public Boolean TryRemove(TKey key, out TValue value); public TValue AddOrUpdate(TKey key, TValue addValue, Func<TKey, TValue> updateValueFactory); public TValue GetOrAdd(TKey key, TValue value); public Int32 Count { get; } public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator(); }

Muhim xususiyatlar

  • Barcha to'plamlar non-blocking: Agar thread elementni olishga harakat qilsa va element mavjud bo'lmasa, thread bloklanmaydi — darhol qaytadi. Shu sababli metodlar TryDequeue, TryPop, TryTake, TryGetValue deb nomlanadi — element olinsa true, olinmasa false qaytaradi.
  • Ichki implementatsiya:
    • ConcurrentDictionary — ichki ravishda Monitor ishlatadi (qisqa vaqt)
    • ConcurrentQueue va ConcurrentStack — lock-free, Interlocked metodlari bilan
    • ConcurrentBag — har bir thread uchun mini-kolleksiya, Interlocked metodlari bilan
  • GetEnumerator: ConcurrentStack, ConcurrentQueue va ConcurrentBag uchun to'plam snapshot olinadi. ConcurrentDictionary uchun esa snapshot olinmaydi — enumeratsiya vaqtida to'plam o'zgarishi mumkin.

IProducerConsumerCollection interfeysi

ConcurrentStack, ConcurrentQueue va ConcurrentBag — bu uchala klass IProducerConsumerCollection<T> interfeysini amalga oshiradi:

C# public interface IProducerConsumerCollection<T> : IEnumerable<T>, ICollection, IEnumerable { Boolean TryAdd(T item); Boolean TryTake(out T item); T[] ToArray(); void CopyTo(T[] array, Int32 index); }

Bu interfeysni amalga oshirgan har qanday klass blocking to'plamga aylantirilishi mumkin — ishlab chiqaruvchilar (producers) to'plam to'lganda bloklanadi, iste'molchilar (consumers) to'plam bo'shaganda bloklanadi.

BlockingCollection<T>

Non-blocking to'plamni blocking to'plamga aylantirish uchun System.Collections.Concurrent.BlockingCollection<T> klassini ishlatish mumkin:

C# public class BlockingCollection<T> : IEnumerable<T>, ICollection, IEnumerable, IDisposable { public BlockingCollection(IProducerConsumerCollection<T> collection, Int32 boundedCapacity); public void Add(T item); public Boolean TryAdd(T item, Int32 msTimeout, CancellationToken cancellationToken); public void CompleteAdding(); public T Take(); public Boolean TryTake(out T item, Int32 msTimeout, CancellationToken cancellationToken); public Int32 BoundedCapacity { get; } public Int32 Count { get; } public Boolean IsAddingCompleted { get; } // true if CompleteAdding is called public Boolean IsCompleted { get; } // true if IsAddingComplete is true and Count==0 public IEnumerable<T> GetConsumingEnumerable(CancellationToken cancellationToken); public void CopyTo(T[] array, int index); public T[] ToArray(); public void Dispose(); }

boundedCapacity parametri to'plamdagi elementlarning maksimal sonini belgilaydi. Add chaqirilganda to'plam to'liq bo'lsa, ishlab chiqaruvchi thread bloklanadi. BlockingCollection ichki ravishda ikkita SemaphoreSlim obyektidan foydalanib, ishlab chiqaruvchilar va iste'molchilarni bloklaydi.

Ishlab chiqaruvchilar boshqa elementlar qo'shilmasligini bildirish uchun CompleteAdding metodini chaqirishi kerak. Bu GetConsumingEnumerable dan foydalanayotgan foreach loopni tugatadi.

Producer/Consumer namunasi

C# public static void Main() { var bl = new BlockingCollection<Int32>(new ConcurrentQueue<Int32>()); // A thread pool thread will do the consuming ThreadPool.QueueUserWorkItem(ConsumeItems, bl); // Add 5 items to the collection for (Int32 item = 0; item < 5; item++) { Console.WriteLine("Producing: " + item); bl.Add(item); } // Tell the consuming thread(s) that no more items will be added to the collection bl.CompleteAdding(); Console.ReadLine(); // For testing purposes } private static void ConsumeItems(Object o) { var bl = (BlockingCollection<Int32>) o; // Block until an item shows up, then process it foreach (var item in bl.GetConsumingEnumerable()) { Console.WriteLine("Consuming: " + item); } // The collection is empty and no more items are going into it Console.WriteLine("All items have been consumed"); }

Natija:

Producing: 0 Producing: 1 Producing: 2 Producing: 3 Producing: 4 Consuming: 0 Consuming: 1 Consuming: 2 Consuming: 3 Consuming: 4 All items have been consumed

O'zingiz ishga tushirsangiz, Producing va Consuming satrlari aralash chiqishi mumkin, lekin All items have been consumed har doim oxirida bo'ladi.

BlockingCollection qo'shimcha metodlari

BlockingCollection shuningdek statik AddToAny, TryAddToAny, TakeFromAny va TryTakeFromAny metodlarini ham taklif qiladi. Bularning barchasi BlockingCollection<T>[] massivini qabul qiladi. (Try)AddToAny metodlari massivdagi to'plamlarni aylanib, elementni qabul qila oladigan birinchi to'plamni topadi. (Try)TakeFromAny metodlari esa massivdagi to'plamlarni aylanib, elementni olish mumkin bo'lgan birinchi to'plamni topadi.