30-Bob: Gibrid Thread Sinxronizatsiya Konstruktsiyalari
User-mode va kernel-mode konstruktsiyalarini birlashtirib, yuqori unumdorlikli qulflar yaratish
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.
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:
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.
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
Enterchaqiruvi uchun mosLeavechaqiruvi bo'lishi kerak.Mutexbu xususiyatga ega qulf namunasidir.
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:
Bu qulf SimpleHybridLock ga qaraganda ko'proq maydonlarga ega va mantiqiy murakkab. Keling, Enter metodining ishlashini qadam-baqadam ko'rib chiqamiz:
- Rekursiya tekshiruvi: Birinchi navbatda, chaqiruvchi threadning ID si qulfni egallaydigan thread ID si bilan taqqoslanadi. Agar mos kelsa, rekursiya hisoblagichi oshiriladi va darhol qaytiladi.
- Spinning: Thread
m_spincountmarta aylanib, qulf bo'shashini kutadi. Har bir aylanishdaInterlocked.CompareExchangebilan qulfni egallashga harakat qiladi. - Kernel-mode kutish: Agar spinning tugagach ham qulf olinmasa,
Interlocked.IncrementvaWaitOneorqali kernel rejimida kutiladi.
Unumdorlik taqqoslash
Quyida turli qulflarning unumdorlik taqqoslash natijalari keltirilgan (tezdan sekinroqqa):
| Qulf turi | Vaqt (x oshirish) | Natija |
|---|---|---|
Incrementing x: 8 | — | Eng tez (qulfsiz) |
Monitor (M): 69 | ~9x | Sekinroq |
SpinLock: 164 | ~21x | Sekinroq |
SimpleHybridLock: 164 | ~21x | SpinLock ga o'xshash |
AnotherHybridLock: 230 | ~29x | Egalik/rekursiya tufayli |
SimpleWaitLock: 8,854 | ~1,107x | Eng 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)
Waitmetodlari timeout vaCancellationTokenni qabul qiladi
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
lockkalit 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:
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.Enterchaqirilganda, CLR massivda bo'sh sync blockni topadi va obyektning sync block indeksini shu blokka yo'naltiradiMonitor.Exitchaqirilganda, agar boshqa kutayotgan threadlar bo'lmasa, sync block indeksi qaytadan -1 ga o'rnatiladi
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
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)
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:
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.
this ga qulf qo'yish o'rniga, har doim xususiy obyekt yaratib, unga qulf qo'ying.
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:
MarshalByRefObjectdan hosil bo'lgan turga havola o'tkazilganda, siz haqiqiy obyekt emas, balki proksi obyektni qulflamoqdasiz. - Domain-neytral turlar: Turga havola
Monitor.Enterga 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
Stringobyektiga havola qilishi va bir-birini bilmasdan sinxronizatsiya qilishi mumkin. - Value turlar:
MonitormetodlariObjectqabul qilganligi sababli, value turni o'tkazish boxing ga olib keladi. Har safar yangi boxed obyekt yaratiladi vaMonitor.Enterhar safar boshqa obyektga qulf qo'yadi — sinxronizatsiya umuman ishlamaydi! - [MethodImpl(MethodImplOptions.Synchronized)]: Bu atributni hech qachon ishlatmang. Instance metod bo'lsa
thisga, 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:
Bu quyidagi kodga ekvivalent:
Muallif lock kalit so'zini ishlatmaslikni maslahat beradi. Buning bir nechta sababi bor:
- finally blokidagi Exit: C# jamoasi qulfni
finallyblokida bo'shatish yaxshi deb hisobladi. Lekin agartrybloki ichida istisno bo'lsa, holat buzilgan bo'lishi mumkin vafinallyblokida qulfni bo'shatish boshqa threadga buzilgan holatga kirishga imkon beradi. Ilovaning osib qolishi (hang) xavfsiz emas holat bilan ishlashdan yaxshiroq. - Unumdorlik:
try/finallyblokiga kirish va chiqish unumdorlikni pasaytiradi. Ba'zi JIT kompilyatorlaritrybloki 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
Mana bu konstruktsiyani ishlatish namunasi:
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.
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:
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 turi | Vaqt | Taqqoslash |
|---|---|---|
OneManyLock: 330 | — | Eng tez |
ReaderWriterLockSlim: 554 | ~1.7x | Sekinroq |
ReaderWriterLock: 984 | ~3x | Eng 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.
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.
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
asyncmetod 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
Monitorklassini xususiy (private) maydon bilan ishlating - Ko'p o'quvchilar va kam yozuvchilar bo'lsa,
Monitoro'rniga reader-writer qulfdan foydalaning — bu ko'p o'quvchilarni bir vaqtda ishlashga imkon beradi - Rekursiv qulflardan qoching — ular unumdorlikni pasaytiradi
- Qulfni
finallyblokida 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.
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:
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:
Singletonuchun xotira ajratish- Konstruktorni chaqirish
- Havolani
s_valuega yozish
Lekin kompilyator buning o'rniga shunday qilishi mumkin:
Singletonuchun xotira ajratish- Havolani
s_valuega yozish (publish) - 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:
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
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:
Natija:
LazyThreadSafetyMode bayroqlari:
Xotira cheklangan stsenariylarda Lazy<T> misolini yaratish o'rniga, System.Threading.LazyInitializer klassining statik metodlaridan foydalanishingiz mumkin:
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:
Mana bu patternning ko'rinishi:
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
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:
Bu yordamida resursga asinxron tarzda (hech qanday threadni bloklamasdan) sinxron kirish mumkin:
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:
Bu klassning misolida ikki TaskScheduler obyekti mavjud bo'lib, ular reader/writer semantikasini ta'minlaydi:
ExclusiveSchedulerorqali rejalashtirilgan vazifalar birma-bir bajariladi, faqatConcurrentSchedulerorqali rejalashtirilgan vazifalar ishlamayotgandaConcurrentSchedulerorqali rejalashtirilgan vazifalar bir vaqtda bajarilishi mumkin, faqatExclusiveSchedulerorqali rejalashtirilgan vazifalar ishlamayotganda
AsyncOneManyLock
Afsuski, .NET Framework reader-writer semantikasiga ega asinxron qulfni taqdim etmaydi. Shuning uchun muallif o'zi AsyncOneManyLock klassini yaratgan. U SemaphoreSlim kabi ishlatiladi:
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:
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,TryGetValuedeb nomlanadi — element olinsatrue, olinmasafalseqaytaradi. - Ichki implementatsiya:
ConcurrentDictionary— ichki ravishdaMonitorishlatadi (qisqa vaqt)ConcurrentQueuevaConcurrentStack— lock-free,Interlockedmetodlari bilanConcurrentBag— har bir thread uchun mini-kolleksiya,Interlockedmetodlari bilan
- GetEnumerator:
ConcurrentStack,ConcurrentQueuevaConcurrentBaguchun to'plam snapshot olinadi.ConcurrentDictionaryuchun esa snapshot olinmaydi — enumeratsiya vaqtida to'plam o'zgarishi mumkin.
IProducerConsumerCollection interfeysi
ConcurrentStack, ConcurrentQueue va ConcurrentBag — bu uchala klass IProducerConsumerCollection<T> interfeysini amalga oshiradi:
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:
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
Natija:
O'zingiz ishga tushirsangiz, Producing va Consuming satrlari aralash chiqishi mumkin, lekin All items have been consumed har doim oxirida bo'ladi.
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.