29-Bob: Primitiv Thread Sinxronizatsiya Konstruktsiyalari

Thread sinxronizatsiyasining eng past darajadagi qurilma bloklari: volatile, Interlocked, spinlock, event, semaphore va mutex

~55 daqiqaO'qish vaqti
12 ta mavzuShu bobda
Ilg'orDaraja
757-787 betAsl kitobda

Thread sinxronizatsiya qulflari bilan uchinchi muammo shundaki, ular resursga bir vaqtning o'zida faqat bitta threadga kirish imkonini beradi. Bu qulfning mavjud bo'lishining butun sababi, lekin bu ham muammo, chunki threadni bloklash ko'proq threadlar yaratilishiga sabab bo'ladi. Masalan, agar thread pool threadi qulflash uchun urinsa va uni ola olmasa, thread pool CPUlarni band qilib turish uchun yangi thread yaratishi ehtimoli katta. 26-bobda ("Thread asoslari") muhokama qilinganidek, thread yaratish xotira va unumdorlik jihatidan juda qimmat. Bundan ham yomoni, bloklangan thread qayta ishga tushganda, u yangi thread pool threadi bilan ishlaydi; Windows endi CPUlardan ko'proq threadlarni rejalashtirmoqda va bu kontekst almashtirishni oshiradi, bu esa unumdorlikka salbiy ta'sir qiladi.

Bularning barchasining xulosasi shuki, thread sinxronizatsiyasi yomon, shuning uchun siz ilovalaringizni iloji boricha undan qochishga loyihalashingiz kerak. Buning uchun static maydonlar kabi umumiy ma'lumotlardan qochishingiz kerak. Thread new operatori yordamida obyekt yaratganda, new operatori obyektga havola qaytaradi. Bu vaqtda faqat obyektni yaratgan thread unga havolaga ega; boshqa hech qanday thread bu obyektga kira olmaydi. Agar siz bu havolani yaratuvchi thread bilan bir vaqtda obyektdan foydalanishi mumkin bo'lgan boshqa threadga o'tkazmasangiz, obyektga kirishni sinxronlash zarurati yo'q.

Qiymat turlarini ishlatishga harakat qiling, chunki ular doimo nusxalanadi, shuning uchun har bir thread o'z nusxasida ishlaydi. Va nihoyat, bir nechta threadlarning umumiy ma'lumotlarga bir vaqtda kirishida muammo yo'q — agar kirish faqat o'qish uchun bo'lsa. Masalan, String turi bunga misol: String obyekti yaratilgandan so'ng, u immutable (o'zgarmas); shuning uchun ko'p threadlar bitta String obyektiga String buzilish xavfisiz bir vaqtda kira oladi.

Klass Kutubxonalari va Thread Xavfsizligi

Endi klass kutubxonalari va thread sinxronizatsiyasi haqida qisqacha gapirib o'taman. Microsoft ning Framework Class Library (FCL) barcha statik metodlarning thread-safe ekanligiga kafolat beradi. Bu shuni anglatadiki, agar ikkita thread bir vaqtning o'zida statik metodni chaqirsa, hech qanday ma'lumot buzilmaydi. FCL buni ichki ravishda amalga oshirishi kerak edi, chunki turli assemblylarni ishlab chiqaradigan bir nechta kompaniyalar resursga arbitraj kirish uchun bitta qulf bo'yicha kelishib olishlarining iloji yo'q. Console klassi ichida static maydon bor va ko'p metodlari konsolga bir vaqtda faqat bitta thread kirishini ta'minlash uchun uni oladi va bo'shatadi.

Ma'lumot uchun aytilsa, metodni thread-safe qilish ichki ravishda thread sinxronizatsiya qulfini olishini anglatmaydi. Thread-safe metod shuni anglatadiki, ikkita thread bir vaqtda ma'lumotlarga kirishga harakat qilsa, ma'lumotlar buzilmaydi. System.Math klassi quyidagi kabi statik Max metodiga ega:

C# public static Int32 Max(Int32 val1, Int32 val2) { return (val1 < val2) ? val2 : val1; }

Bu metod hech qanday qulf olmasa ham thread-safe. Chunki Int32 qiymat turi bo'lgani uchun, Max ga uzatilgan ikkita Int32 qiymat unga nusxalanadi va shuning uchun bir nechta threadlar bir vaqtda Max ni chaqirishi mumkin, lekin har bir thread o'z ma'lumotlarida ishlaydi, boshqa threadlardan ajratilgan holda.

Boshqa tomondan, FCL misol (instance) metodlari thread-safe ekanligini kafolatlamaydi, chunki barcha qulflash kodini qo'shish unumdorlikni juda pasaytiradi. Aslida, agar har bir misol metodi qulf olsa, siz oxir-oqibat ilovangizda faqat bitta thread ishlashiga erishasiz, bu unumdorlikni yanada pasaytiradi. Oldinroq aytib o'tilganidek, thread obyektni yaratganda, faqat shu thread obyektga kirishga havolaga ega, va misol metodlarini chaqirishda hech qanday thread sinxronizatsiyasi talab qilinmaydi. Biroq, agar thread havolani boshqa threadlarga ochsa — uni static maydonga qo'yib, ThreadPool.QueueUserWorkItem ga argument sifatida uzatib va hokazo — unda threadlar bir vaqtda faqat o'qish bo'lmagan kirishga urinsa, thread sinxronizatsiyasi talab qilinadi.

Tavsiya

O'z klass kutubxonalaringiz ham shu patternni kuzatishi tavsiya etiladi; ya'ni barcha statik metodlaringizni thread-safe qiling va barcha misol metodlaringizni thread-safe qilmang. Buning bitta istisnosi bor: agar misol metodining maqsadi threadlarni koordinatsiya qilish bo'lsa, u holda misol metodi thread-safe bo'lishi kerak. Masalan, bitta thread CancellationTokenSource ning Cancel metodini chaqirishi va boshqa thread tegishli CancellationToken ning IsCancellationRequested xususiyatini so'rashi mumkin.

Primitiv User-Mode va Kernel-Mode Konstruktsiyalar

Ushbu bobda men primitiv thread sinxronizatsiya konstruktsiyalarini tushuntiraman. Primitiv deganda, men kodingizda foydalanish uchun mavjud bo'lgan eng oddiy konstruktsiyalarni nazarda tutaman. Primitiv konstruktsiyalarning ikki turi mavjud: user-mode va kernel-mode.

Iloji boricha primitiv user-mode konstruktsiyalardan foydalanishingiz kerak, chunki ular kernel-mode konstruktsiyalarga qaraganda sezilarli darajada tezroq, chunki ular threadlarni koordinatsiya qilish uchun maxsus CPU ko'rsatmalaridan foydalanadi. Bu koordinatsiya apparatda sodir bo'layotganini anglatadi (bu uni tez qiladi). Bu shuningdek, Windows operatsion tizimi primitiv user-mode konstruktsiyada bloklangan threadni hech qachon aniqlamasligini anglatadi. User-mode primitiv konstruktsiyada bloklangan thread pool threadi hech qachon bloklangan hisoblanmagani uchun, thread pool vaqtincha bloklangan threadni almashtirish uchun yangi thread yaratmaydi. Bundan tashqari, bu CPU ko'rsatmalari threadni juda qisqa vaqt uchun bloklaydi.

Biroq, kamchiligi bor — faqat Windows operatsion tizim yadrosi threadni ishlashdan to'xtatishi mumkin, shunda u CPU vaqtini sarflamaydi. User mode da ishlaydigan thread tizim tomonidan oldindan olib tashlanishi (preempted) mumkin, lekin thread iloji boricha tezroq qayta rejalashtiriladi. Shunday qilib, resurs olmoqchi bo'lgan, lekin ola olmayotgan thread user mode da aylanadi (spin). Bu ko'p CPU vaqtini behuda sarflaydi.

Bu bizni primitiv kernel-mode konstruktsiyalarga olib keladi. Kernel-mode konstruktsiyalar Windows operatsion tizimining o'zi tomonidan ta'minlanadi. Shu sababli, ular ilovangiz threadlarining OT yadroda amalga oshirilgan funksiyalarni chaqirishini talab qiladi. Threadlarni user mode dan kernel mode ga va orqaga o'tkazish katta unumdorlik zarbasini beradi, shuning uchun kernel-mode konstruktsiyalaridan qochish kerak. Biroq, ularning ijobiy tomoni bor — thread kernel-mode konstruktsiyasidan foydalanib, boshqa thread egalik qilayotgan resursni olishga uringanda, Windows threadni bloklaydi va u endi CPU vaqtini behuda sarflamaydi. Keyin, resurs bo'shaganda, Windows threadni qayta tiklaydi va unga resursga kirishga ruxsat beradi.

Livelock va Deadlock

Konstruktsiyada kutayotgan thread, agar konstruktsiyani hozirda ushlab turgan thread uni hech qachon bo'shatmasa, abadiy bloklangan bo'lishi mumkin. Agar konstruktsiya user-mode bo'lsa, thread CPU da abadiy ishlaydi va biz buni livelock deb ataymiz. Agar konstruktsiya kernel-mode bo'lsa, thread abadiy bloklangan va biz buni deadlock deb ataymiz. Ikkalasi ham yomon, lekin ikkalasi orasida deadlock doimo afzalroq, chunki livelock CPU vaqtini ham, xotirani ham (threadning steki va boshqalar) behuda sarflaydi, deadlock esa faqat xotirani sarflaydi.

Ideal dunyoda, biz ikkala dunyoning eng yaxshi tomonlarini birlashtiradigan konstruktsiyalarga ega bo'lishni xohlaymiz. Ya'ni, biz tortishuv bo'lmaganda tez va bloklamas (user-mode konstruktsiyalari kabi) va tortishuv bo'lganda operatsion tizim yadrosi tomonidan bloklashni xohlaymiz. Bunday ishlaydigan konstruktsiyalar mavjud; men ularni gibrid (hybrid) konstruktsiyalar deb atayman va ularni 30-bobda muhokama qilaman.

CLR ning thread sinxronizatsiya konstruktsiyalarining ko'pchiligi aslida Win32 thread sinxronizatsiya konstruktsiyalari atrofidagi obyektga yo'naltirilgan klass wrapperlardir. Nihoyat, CLR threadlari Windows threadlaridir, ya'ni Windows threadlarni rejalashtiradi va boshqaradi.

User-Mode Konstruktsiyalar

CLR quyidagi ma'lumot turlari o'zgaruvchilariga o'qish va yozishning atomik ekanligini kafolatlaydi: Boolean, Char, (S)Byte, (U)Int16, (U)Int32, (U)IntPtr, Single va reference turlari. Bu shuni anglatadiki, ushbu o'zgaruvchi ichidagi barcha baytlar bir vaqtda o'qiladi yoki yoziladi. Masalan, agar sizda quyidagi klass bo'lsa:

C# internal static class SomeType { public static Int32 x = 0; }

va agar thread quyidagi kod satrini bajarsa:

C# SomeType.x = 0x01234567;

u holda x o'zgaruvchisi 0x00000000 dan 0x01234567 ga birdaniga (atomik ravishda) o'zgaradi. Boshqa thread oraliq holatdagi qiymatni ko'rishi mumkin emas. Masalan, boshqa threadning SomeType.x ni so'rab 0x01230000 qiymatini olishi mumkin emas.

Endi faraz qilaylik, oldingi SomeType klassidagi x maydoni Int64 bo'lsin. Agar thread quyidagi kodni bajarsa:

C# SomeType.x = 0x0123456789abcdef;

boshqa thread x ni so'rab 0x0123456700000000 yoki 0x0000000089abcdef qiymatini olishi mumkin, chunki o'qish va yozish operatsiyalari atomik emas. Bu torn read (yirtilgan o'qish) deb ataladi.

O'zgaruvchiga atomik kirish o'qish yoki yozishning birdaniga sodir bo'lishini kafolatlasa-da, kompilyator va CPU optimizatsiyalari tufayli o'qish yoki yozishning qachon sodir bo'lishini kafolatlamaydi. Ushbu bo'limda muhokama qilingan primitiv user-mode konstruktsiyalar bu atomik o'qish va yozish operatsiyalarining vaqtini boshqarish uchun ishlatiladi.

Primitiv user-mode thread sinxronizatsiya konstruktsiyalarining ikki turi mavjud:

  • Volatile konstruktsiyalar — muayyan vaqtda oddiy ma'lumot turini o'z ichiga olgan o'zgaruvchi ustida atomik o'qish yoki yozish operatsiyasini bajaradi
  • Interlocked konstruktsiyalar — muayyan vaqtda oddiy ma'lumot turini o'z ichiga olgan o'zgaruvchi ustida atomik o'qish va yozish operatsiyasini bajaradi

Barcha volatile va interlocked konstruktsiyalari oddiy ma'lumot turini o'z ichiga olgan o'zgaruvchiga havola (xotira manzili) uzatishingizni talab qiladi.

Volatile Konstruktsiyalar

Hisoblashning dastlabki kunlarida dasturiy ta'minot assembly tilida yozilgan. Assembly tili juda zerikarli, chunki dasturchilar hamma narsani aniq ko'rsatishlari kerak — shu CPU registeridan foydalaning, u yerga tarmoqlaning va hokazo. Dasturlashni soddalashtirish uchun yuqori darajadagi tillar yaratildi. Bu tillar if/else, switch/case, turli sikllar, lokal o'zgaruvchilar, argumentlar, virtual metod chaqiruvlari, operator ortiqcha yuklash va boshqa ko'plab foydali konstruktsiyalarni taqdim etdi. Oxir-oqibat, bu til kompilyatorlari yuqori darajadagi konstruktsiyalarni past darajadagi konstruktsiyalarga aylantirishi kerak.

Boshqacha aytganda, C# kompilyatori C# konstruktsiyalaringizni Intermediate Language (IL) ga tarjima qiladi, bu esa so'ngra just-in-time (JIT) kompilyator tomonidan native CPU ko'rsatmalariga aylantiriladi, bu esa so'ngra CPU o'zi tomonidan qayta ishlanishi kerak. Bundan tashqari, C# kompilyatori, JIT kompilyator va hatto CPU o'zi ham kodingizni optimallashtirishi mumkin. Masalan, quyidagi kulgili metod oxir-oqibat umuman hech narsaga kompilyatsiya qilinishi mumkin:

C# private static void OptimizedAway() { // Konstantan ifoda kompilyatsiya vaqtida hisoblanadi va nolga teng Int32 value = (1 * 100) - (50 * 2); // Agar qiymat 0 bo'lsa, sikl hech qachon bajarilmaydi for (Int32 x = 0; x < value; x++) { // Sikl ichidagi kodni kompilyatsiya qilish shart emas Console.WriteLine("Jeff"); } }

Bu kodda kompilyator value har doim 0 bo'lishini ko'radi; shuning uchun sikl hech qachon bajarilmaydi va demak, sikl ichidagi kodni kompilyatsiya qilish shart emas. Bu metod hech narsaga aylanishi mumkin.

C# kompilyatori, JIT kompilyator va CPU kodimizni optimallashtirganda, ular kodning niyati saqlanishiga kafolat beradi. Ya'ni, bitta threadli nuqtai nazardan, metod biz xohlagan narsani qiladi, garchi uni biz manba kodida tasvirlaganimizdek bajarmasligi mumkin. Biroq, ko'p threadli nuqtai nazardan niyat saqlanmasligi mumkin. Quyida optimizatsiyalar dastur kutilganidek ishlamasligiga olib keladigan misol keltirilgan:

C# internal static class StrangeBehavior { // Keyinroq ko'rib chiqamiz, muammoni tuzatish uchun // bu maydonni volatile deb belgilang private static Boolean s_stopWorker = false; public static void Main() { Console.WriteLine("Main: letting worker run for 5 seconds"); Thread t = new Thread(Worker); t.Start(); Thread.Sleep(5000); s_stopWorker = true; Console.WriteLine("Main: waiting for worker to stop"); t.Join(); } private static void Worker(Object o) { Int32 x = 0; while (!s_stopWorker) x++; Console.WriteLine("Worker: stopped when x={0}", x); } }

Bu kodda Main metodi Worker metodini bajaradigan yangi thread yaratadi. Worker metodi to'xtatilishdan oldin iloji boricha yuqori raqamgacha sanaydi. Main metodi Worker threadiga 5 soniya ishlashga ruxsat beradi, so'ngra statik Boolean maydonni true qilib o'rnatish orqali to'xtashni buyuradi.

Oddiy ko'rinadi, to'g'rimi? Xo'sh, dasturda barcha optimizatsiyalar tufayli potentsial muammo bor. Ko'ring, Worker metodi kompilyatsiya qilinganda, kompilyator s_stopWorker yoki true yoki false ekanligini ko'radi va bu qiymat Worker metodining ichida hech qachon o'zgarmasligini ham ko'radi. Shuning uchun kompilyator avval s_stopWorker ni tekshiradigan kod ishlab chiqarishi mumkin. Agar s_stopWorker true bo'lsa, Worker: stopped when x=0 ko'rsatiladi. Agar s_stopWorker false bo'lsa, kompilyator x ni abadiy oshiruvchi cheksiz sikl yaratadi. Optimizatsiyalar sikl juda tez ishlashiga sabab bo'ladi, chunki s_stopWorker ni tekshirish faqat sikl boshlanishidan oldin bir marta sodir bo'ladi; u siklning har bir iteratsiyasida tekshirilmaydi.

Diqqat!

Agar siz buni amalda ko'rmoqchi bo'lsangiz, kodni .cs fayliga joylashtiring va C# ning /platform:x86 va /optimize+ kalitlari yordamida kompilyatsiya qiling. Natijadagi EXE faylni ishga tushiring va dastur abadiy ishlashini ko'rasiz. x86 JIT kompilyatori x64 dan ko'ra yetukroq va agressivroq optimizatsiyalar qiladi. Bundan tashqari, debugger ostida ishga tushurmang, chunki debugger JIT kompilyatorni optimallashtirilmagan kod ishlab chiqarishga majbur qiladi.

Keling, ikkita threadning ikkita maydonga kiradigan yana bir misolni ko'rib chiqaylik:

C# internal sealed class ThreadsSharingData { private Int32 m_flag = 0; private Int32 m_value = 0; // Bu metod bitta thread tomonidan bajariladi public void Thread1() { // Eslatma: Bular teskari tartibda bajarilishi mumkin m_value = 5; m_flag = 1; } // Bu metod boshqa thread tomonidan bajariladi public void Thread2() { // Eslatma: m_value m_flag dan oldin o'qilishi mumkin if (m_flag == 1) Console.WriteLine(m_value); } }

Bu koddagi muammo shundaki, kompilyatorlar/CPU kodni shunday tarjima qilishi mumkinki, Thread1 metodidagi ikkita qator tartibini teskari o'zgartirishi mumkin. Axir, ikkita qatorni teskari o'zgartirish metodning niyatini o'zgartirmaydi. Metod m_value ga 5 va m_flag ga 1 qo'yishi kerak. Bitta threadli ilovaning nuqtai nazaridan, bu kodni bajarish tartibi ahamiyatsiz. Agar bu ikkita qator teskari tartibda bajarilsa, boshqa thread Thread2 metodini bajargan holda m_flag 1 ekanligini, lekin keyin 0 ko'rsatishini ko'rishi mumkin.

Bu hamma narsa juda qo'rqinchli va release buildda debug buildga qaraganda muammo paydo bo'lishi ko'proq ehtimol. Endi kodingizni qanday to'g'irlashni gapiraylik.

Statik System.Threading.Volatile klassi quyidagi ikkita statik metodni taklif qiladi:

C# public static class Volatile { public static void Write(ref Int32 location, Int32 value); public static Int32 Read(ref Int32 location); }

Bu metodlar maxsus. Aslida, bu metodlar C# kompilyator, JIT kompilyator va CPU tomonidan odatda bajariladigan ba'zi optimizatsiyalarni o'chiradi. Metodlar quyidagicha ishlaydi:

  • Volatile.Write metodi qiymatni chaqiruv nuqtasida location ga yozilishini majbur qiladi. Bundan tashqari, Volatile.Write chaqiruvidan oldingi har qanday dastur tartibidagi yuklash va saqlash Volatile.Write chaqiruvidan oldin sodir bo'lishi kerak.
  • Volatile.Read metodi qiymatni chaqiruv nuqtasida location dan o'qilishini majbur qiladi. Bundan tashqari, Volatile.Read chaqiruvidan keyingi har qanday dastur tartibidagi yuklash va saqlash Volatile.Read chaqiruvidan keyin sodir bo'lishi kerak.
Muhim qoida

Bilaman, bu juda chalkash bo'lishi mumkin, shuning uchun uni oddiy qoida sifatida umumlashtiray. Threadlar umumiy xotira orqali bir-biri bilan muloqot qilganda, oxirgi qiymatni Volatile.Write ni chaqirish orqali yozing va birinchi qiymatni Volatile.Read ni chaqirish orqali o'qing.

Endi biz ThreadsSharingData klassini ushbu metodlar yordamida to'g'irlashimiz mumkin:

C# internal sealed class ThreadsSharingData { private Int32 m_flag = 0; private Int32 m_value = 0; // Bu metod bitta thread tomonidan bajariladi public void Thread1() { // Eslatma: 5 m_value ga m_flag ga 1 yozilishidan oldin yozilishi kerak m_value = 5; Volatile.Write(ref m_flag, 1); } // Bu metod boshqa thread tomonidan bajariladi public void Thread2() { // Eslatma: m_value m_flag o'qilganidan keyin o'qilishi kerak if (Volatile.Read(ref m_flag) == 1) Console.WriteLine(m_value); } }

Thread1 metodi uchun Volatile.Write chaqiruvi barcha yuqoridagi yozishlar m_flag ga 1 yozilishidan oldin tugashini ta'minlaydi. m_value = 5 Volatile.Write chaqiruvidan oldin bo'lgani uchun, u avval tugashi kerak.

Thread2 metodi uchun Volatile.Read chaqiruvi undan keyingi barcha o'zgaruvchi o'qishlarning m_flag qiymati o'qilganidan keyin boshlanishini ta'minlaydi. m_value ni o'qish Volatile.Read chaqiruvidan keyin bo'lgani uchun, u m_flag qiymati o'qilganidan keyin o'qilishi kerak.

C# ning Volatile Maydonlar uchun Qo'llab-quvvatlashi

Dasturchilar Volatile.Read va Volatile.Write metodlarini to'g'ri chaqirishini ta'minlash juda ko'p ish talab qiladi. Dasturchilar uchun bularning barchasini eslab qolish va boshqa threadlar fon rejimida umumiy ma'lumotlar bilan nima qilayotganini tasavvur qilishni boshlash qiyin. Buni soddalashtirish uchun C# kompilyatori volatile kalit so'ziga ega bo'lib, u quyidagi turlardagi statik yoki misol maydonlariga qo'llanilishi mumkin: Boolean, (S)Byte, (U)Int16, (U)Int32, (U)IntPtr, Single yoki Char. Shuningdek, volatile ni reference turlariga va asosiy turi (S)Byte, (U)Int16 yoki (U)Int32 bo'lgan har qanday enum maydoniga qo'llashingiz mumkin.

JIT kompilyator volatile maydonga barcha kirishlarning volatile o'qish va yozish sifatida bajarilishini ta'minlaydi, shuning uchun Volatile ning statik Read yoki Write metodlarini aniq chaqirish shart emas. Bundan tashqari, volatile kalit so'zi C# va JIT kompilyatorlarga maydonni CPU registerida keshlamaslikni aytadi, bu barcha o'qish va yozishlarning aslida xotiradan sodir bo'lishini ta'minlaydi.

volatile kalit so'zidan foydalanib, ThreadsSharingData klassini quyidagicha qayta yozishimiz mumkin:

C# internal sealed class ThreadsSharingData { private volatile Int32 m_flag = 0; private Int32 m_value = 0; // Bu metod bitta thread tomonidan bajariladi public void Thread1() { // Eslatma: 5 m_value ga m_flag ga 1 yozilishidan oldin yozilishi kerak m_value = 5; m_flag = 1; } // Bu metod boshqa thread tomonidan bajariladi public void Thread2() { // Eslatma: m_value m_flag o'qilganidan keyin o'qilishi kerak if (m_flag == 1) Console.WriteLine(m_value); } }

Ba'zi dasturchilar (shu jumladan men ham) C# ning volatile kalit so'zini yoqtirmaydilar va tilning uni taklif qilmasligi kerak deb o'ylaydilar. Bizning fikrimizcha, aksariyat algoritmlar maydonga kam sonli volatile o'qish yoki yozish operatsiyalarini talab qiladi va maydonga boshqa kirishlarning ko'pchiligi odatiy tarzda sodir bo'lishi mumkin. Masalan, quyidagi algoritmga volatile o'qish operatsiyalarini qanday qo'llashni tushunish qiyin:

C# m_amount = m_amount + m_amount; // m_amount klassda volatile maydon deb faraz qiling

Odatda, butun sonni ikki baravar oshirish barcha bitlarni 1 ga chapga siljitish orqali amalga oshirilishi mumkin va ko'p kompilyatorlar bu kodni ko'rib chiqib, bu optimizatsiyani bajarishi mumkin. Biroq, agar m_amount volatile maydon bo'lsa, bu optimizatsiyaga ruxsat berilmaydi. Kompilyator m_amount ni o'qish uchun registerga yuklaydigan, so'ngra uni yana boshqa registerga o'qiydigan, ikkita registrni qo'shadigan va natijani m_amount maydoniga qaytarib yozadigan kod ishlab chiqarishi kerak.

Bundan tashqari, C# volatile maydonni metodga reference bo'yicha uzatishni qo'llab-quvvatlamaydi. Masalan, agar m_amount volatile Int32 sifatida aniqlangan bo'lsa, Int32.TryParse ni chaqirishga urinish kompilyatorni ogohlantirish chiqarishga majbur qiladi:

C# Boolean success = Int32.TryParse("123", out m_amount); // Oldingi qator C# kompilyatoriga ogohlantirish chiqartiradi: // CS0420: volatile maydon uchun havola volatile sifatida ko'rilmaydi

Nihoyat, volatile maydonlar Common Language Specification (CLS) ga mos kelmaydi, chunki ko'p tillar (shu jumladan Visual Basic) ularni qo'llab-quvvatlamaydi.

Interlocked Konstruktsiyalar

Volatile ning Read metodi atomik o'qish operatsiyasini, Write metodi esa atomik yozish operatsiyasini bajaradi. Ya'ni, har bir metod atomik o'qish yoki atomik yozish operatsiyasini bajaradi. Ushbu bo'limda biz statik System.Threading.Interlocked klassining metodlarini ko'rib chiqamiz. Interlocked klassidagi har bir metod atomik o'qish va yozish operatsiyasini bajaradi. Bundan tashqari, barcha Interlocked metodlari to'liq xotira bariyerlaridir (memory fence). Ya'ni, Interlocked metodiga chaqiruvdan oldingi har qanday o'zgaruvchi yozishlar Interlocked metodi chaqiruvidan oldin bajariladi va undan keyingi har qanday o'zgaruvchi o'qishlar chaqiruvdan keyin bajariladi.

Int32 o'zgaruvchilari ustida ishlaydigan statik metodlar eng ko'p ishlatiladigan metodlar. Ularni bu yerda ko'rsataman:

C# public static class Interlocked { // return (++location) public static Int32 Increment(ref Int32 location); // return (--location) public static Int32 Decrement(ref Int32 location); // return (location += value) // Eslatma: value manfiy son bo'lishi mumkin, ayirishga imkon beradi public static Int32 Add(ref Int32 location, Int32 value); // Int32 old = location; location = value; return old; public static Int32 Exchange(ref Int32 location, Int32 value); // Int32 old = location; // if (location == comparand) location = value; // return old; public static Int32 CompareExchange(ref Int32 location, Int32 value, Int32 comparand); ... }

Yuqoridagi metodlarning Int64 qiymatlari ustida ishlaydigan overloadlari ham mavjud. Bundan tashqari, Interlocked klassi Object, IntPtr, Single va Double ni qabul qiladigan Exchange va CompareExchange metodlarini va generik tur class ga cheklangan generik versiyasini taklif qiladi.

Men Interlocked metodlarini yaxshi ko'raman, chunki ular nisbatan tez va siz ular bilan juda ko'p narsani qilishingiz mumkin. Keling, sizga bir nechta veb-serverlarga asinxron so'rov yuborish va qaytarilgan ma'lumotlarni bir vaqtda qayta ishlash uchun Interlocked metodlarini ishlatadigan kodni ko'rsatay:

C# internal sealed class MultiWebRequests { // Bu yordamchi klass barcha asinxron operatsiyalarni koordinatsiya qiladi private AsyncCoordinator m_ac = new AsyncCoordinator(); // So'rov yubormoqchi bo'lgan veb-serverlar to'plami va ularning javoblari private Dictionary<String, Object> m_servers = new Dictionary<String, Object> { { "http://Wintellect.com/", null }, { "http://Microsoft.com/", null }, { "http://1.1.1.1/", null } }; public MultiWebRequests(Int32 timeout = Timeout.Infinite) { // Barcha so'rovlarni asinxron ravishda bir vaqtda boshlash var httpClient = new HttpClient(); foreach (var server in m_servers.Keys) { m_ac.AboutToBegin(1); httpClient.GetByteArrayAsync(server) .ContinueWith(task => ComputeResult(server, task)); } // AsyncCoordinator ga barcha operatsiyalar boshlangani va // hammasidan tugaganda, Cancel chaqirilganda yoki timeout bo'lganda // AllDone ni chaqirish kerakligini aytish m_ac.AllBegun(AllDone, timeout); } private void ComputeResult(String server, Task<Byte[]> task) { Object result; if (task.Exception != null) { result = task.Exception.InnerException; } else { // I/O yakunlangandan keyin thread pool threadlarida qayta ishlash result = task.Result.Length; // Bu misol faqat uzunlikni qaytaradi } // Natijani saqlash va 1 ta operatsiya tugaganini bildirish m_servers[server] = result; m_ac.JustEnded(); } // Bu metod natijalar endi muhim emasligini bildiradi public void Cancel() { m_ac.Cancel(); } // Bu metod barcha veb-serverlar javob bergandan, Cancel chaqirilgandan // yoki timeout bo'lgandan keyin chaqiriladi private void AllDone(CoordinationStatus status) { switch (status) { case CoordinationStatus.Cancel: Console.WriteLine("Operation canceled."); break; case CoordinationStatus.Timeout: Console.WriteLine("Operation timed-out."); break; case CoordinationStatus.AllDone: Console.WriteLine("Operation completed; results below:"); foreach (var server in m_servers) { Console.Write("{0} ", server.Key); Object result = server.Value; if (result is Exception) { Console.WriteLine("failed due to {0}.", result.GetType().Name); } else { Console.WriteLine("returned {0:N0} bytes.", result); } } break; } } }

AsyncCoordinator klassi barcha thread koordinatsiya mantiqini o'z ichiga oladi. U hamma narsa uchun Interlocked metodlarini ishlatib, kodning juda tez ishlashini va hech qanday threadning hech qachon bloklanmasligini ta'minlaydi. Mana shu klass uchun kod:

C# internal enum CoordinationStatus { AllDone, Timeout, Cancel }; internal sealed class AsyncCoordinator { private Int32 m_opCount = 1; // AllBegun JustEnded chaqirganda kamaytiradi private Int32 m_statusReported = 0; // 0=false, 1=true private Action<CoordinationStatus> m_callback; private Timer m_timer; // Bu metod operatsiya boshlanishidan OLDIN chaqirilishi KERAK public void AboutToBegin(Int32 opsToAdd = 1) { Interlocked.Add(ref m_opCount, opsToAdd); } // Bu metod operatsiya natijasi qayta ishlangandan KEYIN chaqirilishi KERAK public void JustEnded() { if (Interlocked.Decrement(ref m_opCount) == 0) ReportStatus(CoordinationStatus.AllDone); } // Bu metod BARCHA operatsiyalar boshlangandan KEYIN chaqirilishi KERAK public void AllBegun(Action<CoordinationStatus> callback, Int32 timeout = Timeout.Infinite) { m_callback = callback; if (timeout != Timeout.Infinite) m_timer = new Timer(TimeExpired, null, timeout, Timeout.Infinite); JustEnded(); } private void TimeExpired(Object o) { ReportStatus(CoordinationStatus.Timeout); } public void Cancel() { ReportStatus(CoordinationStatus.Cancel); } private void ReportStatus(CoordinationStatus status) { // Agar holat hech qachon xabar qilinmagan bo'lsa, xabar bering; aks holda e'tiborsiz qoldiring if (Interlocked.Exchange(ref m_statusReported, 1) == 0) m_callback(status); } }
Eslatma

m_opCount maydoni 1 ga (0 emas) ishga tushirilgan, bu juda muhim, chunki u konstruktor metodi hali ham veb-server so'rovlarini yuborayotgan paytda AllDone chaqirilmasligini ta'minlaydi. Konstruktor AllBegun ni chaqirmasdan oldin, m_opCount hech qachon 0 ga yetmaydi. Konstruktor AllBegun ni chaqirganda, AllBegun ichki ravishda JustEnded ni chaqiradi, bu m_opCount ni kamaytiradi va uni 1 ga ishga tushirish samarasini bekor qiladi. Endi m_opCount 0 ga yetishi mumkin, lekin faqat barcha veb-server so'rovlari boshlangan bo'lsa.

ReportStatus metodi barcha operatsiyalar tugatilishi, timeout sodir bo'lishi va Cancel chaqirilishi o'rtasidagi poyga holatini hal qiladi. ReportStatus m_callback metodining faqat bir marta chaqirilishini ta'minlashi kerak. G'olibni aniqlash Interlocked.Exchange ni chaqirish orqali amalga oshiriladi. Faqat Interlocked.Exchange 0 qaytargan birinchi thread g'olib bo'ladi va callback metodini chaqiradi.

Interlocked Anything Pattern

Ko'p odamlar Interlocked metodlariga qaraydilar va Microsoft nima uchun ko'proq stsenariylar uchun ishlatilishi mumkin bo'lgan boyroq interlocked metodlar to'plamini yaratmaganiga hayron bo'lishadi. Masalan, Interlocked klassi Multiply, Divide, Minimum, Maximum, And, Or, Xor va boshqa ko'plab metodlarni taklif qilgani yaxshi bo'lardi. Garchi Interlocked klassi bu metodlarni taklif qilmasa-da, Interlocked.CompareExchange yordamida Int32 da istalgan operatsiyani atomik tarzda bajarishga imkon beradigan mashhur pattern mavjud.

Bu pattern ma'lumotlar bazasi yozuvlarini o'zgartirish uchun ishlatiladigan optimistik parallellik patternlariga o'xshaydi. Mana shu pattern atomik Maximum metodini yaratish uchun ishlatilayotgan misol:

C# public static Int32 Maximum(ref Int32 target, Int32 value) { Int32 currentVal = target, startVal, desiredVal; // Loop ichida target ga faqat boshqa thread uni o'zgartirishi // mumkin bo'lgan holda kirmang do { // Ushbu iteratsiyaning boshlang'ich qiymatini yozib oling startVal = currentVal; // Kerakli qiymatni startVal va value asosida hisoblang desiredVal = Math.Max(startVal, value); // ESLATMA: thread bu yerda preempted bo'lishi mumkin! // if (target == startVal) target = desiredVal // O'zgarishdan oldingi qiymat qaytariladi currentVal = Interlocked.CompareExchange(ref target, desiredVal, startVal); // Agar boshlang'ich qiymat shu iteratsiya davomida o'zgargan bo'lsa, takrorlang } while (startVal != currentVal); // Ushbu thread o'rnatishga uringan paytdagi maksimal qiymatni qaytaring return desiredVal; }

Metod boshida currentVal target dagi qiymat bilan ishga tushiriladi. Keyin, sikl ichida startVal shu qiymatga o'rnatiladi. startVal dan foydalanib, istalgan operatsiyani bajarishingiz mumkin. Oxir-oqibat, natijani desiredVal ga joylashtirishingiz kerak. Interlocked.CompareExchange target dagi qiymat startVal ga mos kelishini tekshiradi. Agar mos kelsa, CompareExchange uni desiredVal ga o'zgartiradi. Agar mos kelmasa, boshqa thread target ni o'zgartirgan, shuning uchun while sikli yangi qiymat bilan operatsiyani qaytadan urinadi.

Men bu patternni o'z kodimda ko'p ishlatganman va hatto uni inkapsulyatsiya qiluvchi generik Morph metodi yaratganman:

C# delegate Int32 Morpher<TResult, TArgument>(Int32 startValue, TArgument argument, out TResult morphResult); static TResult Morph<TResult, TArgument>(ref Int32 target, TArgument argument, Morpher<TResult, TArgument> morpher) { TResult morphResult; Int32 currentVal = target, startVal, desiredVal; do { startVal = currentVal; desiredVal = morpher(startVal, argument, out morphResult); currentVal = Interlocked.CompareExchange(ref target, desiredVal, startVal); } while (startVal != currentVal); return morphResult; }

Oddiy Spin Lock ni Amalga Oshirish

Lock: Thread Sinxronizatsiyasi
Muhim Resurs (lock)
Thread 1
ichkarida
Thread 2
kutmoqda...
Faqat bitta thread lock ichiga kira oladi. Boshqasi kutadi. Kuzating: ular navbatma-navbat almashadi.

Interlocked metodlari ajoyib, lekin ularning ko'pi faqat Int32 qiymatlari ustida ishlaydi. Agar siz klass obyektining bir nechta maydonlarini atomik tarzda boshqarishingiz kerak bo'lsa? Bunday holda, biz maydonlarni boshqaradigan kod mintaqasiga bir vaqtda faqat bitta threaddan boshqasining kirishini to'xtatishimiz kerak. Interlocked metodlaridan foydalanib, thread sinxronizatsiya qulfini qurish mumkin:

C# internal struct SimpleSpinLock { private Int32 m_ResourceInUse; // 0=false (standart), 1=true public void Enter() { while (true) { // Har doim resursni "ishlatilmoqda" deb belgilang // Ushbu thread uni "ishlatilmayotgan"dan o'zgartirganida, qaytaring if (Interlocked.Exchange(ref m_ResourceInUse, 1) == 0) return; // Black magic shu yerda... } } public void Leave() { // Resursni "ishlatilmayotgan" deb belgilang Volatile.Write(ref m_ResourceInUse, 0); } }

Va uni ishlatish uchun klass:

C# public sealed class SomeResource { private SimpleSpinLock m_sl = new SimpleSpinLock(); public void AccessResource() { m_sl.Enter(); // Bir vaqtning o'zida faqat bitta thread shu yerga kirishi mumkin... m_sl.Leave(); } }

SimpleSpinLock implementatsiyasi juda oddiy. Agar ikkita thread bir vaqtda Enter ni chaqirsa, Interlocked.Exchange bitta threadning m_ResourceInUse ni 0 dan 1 ga o'zgartirishini ta'minlaydi. Bu thread Enter dan qaytadi va AccessResource metodidagi kodni bajarishda davom etadi. Boshqa thread m_ResourceInUse ni 1 dan 1 ga o'zgartiradi, ya'ni u 0 dan o'zgartirmadi, shuning uchun bu thread birinchi thread Leave ni chaqirmaguncha doimiy aylanib (spin), Exchange ni chaqirib turadi.

Bu oddiy thread sinxronizatsiya qulfining implementatsiyasi. Ushbu qulf bilan katta potentsial muammo shundaki, u qulf uchun tortishuv bo'lganda threadlarning aylanishiga sabab bo'ladi. Bu aylanish qimmatbaho CPU vaqtini behuda sarflaydi, CPU ni boshqa foydali ishlarni bajarishdan to'xtatadi. Natijada, spin locklar faqat juda tez bajariladigan kod mintaqalarini himoya qilish uchun ishlatilishi kerak.

Diqqat!

Spin locklar odatda bitta CPU li mashinalarda ishlatilmasligi kerak, chunki qulfni ushlab turgan thread uni tezda bo'shata olmaydi. Holat yana yomonlashadi agar qulfni ushlab turgan thread past prioritetda bo'lsa va qulfni olmoqchi bo'lgan thread yuqori prioritetda bo'lsa — chunki qulfni ushlab turgan thread ishlash imkoniyatini umuman ololmasligi mumkin, bu esa livelock holatiga olib keladi.

FCL System.Threading.SpinLock strukturasini o'z ichiga oladi, bu mening SimpleSpinLock ga o'xshaydi, lekin SpinWait strukturasidan foydalanib unumdorlikni yaxshilaydi. SpinLock strukturasi timeout qo'llab-quvvatlashini ham taklif qiladi. SimpleSpinLock va FCL ning SpinLock i ikkalasi ham qiymat turlari ekanligini ta'kidlash qiziq. Bu ular yengil vaznli, xotiraga qulay obyektlar ekanligini anglatadi.

Thread Jarayonini Kechiktirish

"Black Magic" resurs olmoqchi bo'lgan threadning bajarilishini vaqtincha to'xtatishdir, shunda hozirda resursga ega bo'lgan thread o'z kodini bajarib, resursni bo'shatishi mumkin. Buning uchun SpinWait strukturasi ichki ravishda Thread ning statik Sleep, Yield va SpinWait metodlarini chaqiradi.

Thread.Sleep — thread ma'lum vaqt davomida rejalashtirilmasligini tizimga aytadi:

C# public static void Sleep(Int32 millisecondsTimeout); public static void Sleep(TimeSpan timeout);

Bu metod threadni belgilangan vaqt o'tgunicha o'zini to'xtatishiga sabab bo'ladi. Sleep(0) ni chaqirish chaqiruvchi threadning o'z vaqt bo'lagining qolgan qismini boshqa threadga berishini bildiradi. Sleep(1) esa kontekst almashtirishni majbur qiladi va Windows threadni ichki tizim taymeri aniqligidan ko'proq muddat davomida uxlashga majbur qiladi.

Thread.Yield — joriy CPU da ishlashga tayyor bo'lgan boshqa threadni rejalashtirishni so'raydi:

C# public static Boolean Yield();

Agar Windows joriy protsessorda ishlashga tayyor boshqa threadga ega bo'lsa, Yield true qaytaradi va Yield ni chaqirgan thread erta tugadi, tanlangan thread bitta vaqt bo'lagi uchun ishlaydi, keyin Yield ni chaqirgan thread qayta rejalanadi va yangi vaqt bo'lagi bilan ishlashni boshlaydi. Agar Windows joriy protsessorda ishlashga tayyor boshqa threadga ega bo'lmasa, Yield false qaytaradi va thread o'z vaqt bo'lagini davom ettiradi.

Thread.SpinWait — hyperthreaded CPU larda spin loop bajarayotganda foydali:

C# public static void SpinWait(Int32 iterations);

Bu metodni chaqirish aslida maxsus CPU ko'rsatmasini bajaradi; u Windows ga hech narsa qilishni aytmaydi (chunki Windows allaqachon CPU da ikki thread rejalashtirilgan deb biladi). Hyperthreaded bo'lmagan CPU da bu maxsus ko'rsatma oddiygina e'tibordan chetda qoldiriladi.

Kernel-Mode Konstruktsiyalar

Windows threadlarni sinxronlash uchun bir nechta kernel-mode konstruktsiyalarini taklif qiladi. Kernel-mode konstruktsiyalar user-mode konstruktsiyalardan ancha sekin. Buning sababi ular Windows operatsion tizimning o'zidan koordinatsiyani talab qiladi. Bundan tashqari, kernel obyektiga har bir metod chaqiruvi chaqiruvchi threadni managed koddan native user-mode kodga, so'ngra native kernel-mode kodga o'tkazadi va keyin butun yo'lni qaytadi. Bu o'tishlar ko'p CPU vaqtini talab qiladi va tez-tez bajarilsa, ilovangizning umumiy unumdorligiga salbiy ta'sir qilishi mumkin.

Biroq, kernel-mode konstruktsiyalar primitiv user-mode konstruktsiyalariga nisbatan ba'zi afzalliklarni taklif qiladi:

  • Kernel-mode konstruktsiyasi resursga tortishuv aniqlaganda, Windows yutqazgan threadni bloklaydi, shunda u CPU da aylanmaydi va protsessor resurslarini behuda sarflamaydi.
  • Kernel-mode konstruktsiyalar native va managed threadlarni sinxronlashtirishi mumkin.
  • Kernel-mode konstruktsiyalar bitta mashinada turli jarayonlarda ishlaydigan threadlarni sinxronlashtirishi mumkin.
  • Kernel-mode konstruktsiyalarga ruxsatsiz hisoblardan kirishni oldini olish uchun xavfsizlik qo'llanilishi mumkin.
  • Thread to'plamdagi barcha kernel-mode konstruktsiyalar mavjud bo'lguncha yoki ulardan biri mavjud bo'lguncha bloklashi mumkin.
  • Thread timeout qiymati bilan kernel-mode konstruktsiyasida bloklashi mumkin; agar thread belgilangan vaqtda resursga kira olmasa, thread blokdan chiqariladi.

Ikkita primitiv kernel-mode thread sinxronizatsiya konstruktsiyalari eventlar va semaforlardir. Boshqa kernel-mode konstruktsiyalar, masalan, mutex, bu ikki primitiv konstruktsiya ustiga qurilgan.

System.Threading namespace i abstrakt bazaviy klass WaitHandle ni taklif qiladi. WaitHandle klassi Win32 kernel obyekt handle ni o'rab oladigan oddiy klass. FCL WaitHandle dan hosila qilingan bir nechta klasslarni taqdim etadi. Klass iyerarxiyasi quyidagicha ko'rinadi:

Iyerarxiya WaitHandle EventWaitHandle AutoResetEvent ManualResetEvent Semaphore Mutex

WaitHandle bazaviy klassining eng muhim ochiq metodlari quyidagilar:

C# public abstract class WaitHandle : MarshalByRefObject, IDisposable { // WaitOne ichki ravishda Win32 WaitForSingleObjectEx funksiyasini chaqiradi public virtual Boolean WaitOne(); public virtual Boolean WaitOne(Int32 millisecondsTimeout); public virtual Boolean WaitOne(TimeSpan timeout); // WaitAll ichki ravishda Win32 WaitForMultipleObjectsEx funksiyasini chaqiradi public static Boolean WaitAll(WaitHandle[] waitHandles); public static Boolean WaitAll(WaitHandle[] waitHandles, Int32 millisecondsTimeout); public static Boolean WaitAll(WaitHandle[] waitHandles, TimeSpan timeout); // WaitAny ichki ravishda Win32 WaitForMultipleObjectsEx funksiyasini chaqiradi public static Int32 WaitAny(WaitHandle[] waitHandles); public static Int32 WaitAny(WaitHandle[] waitHandles, Int32 millisecondsTimeout); public static Int32 WaitAny(WaitHandle[] waitHandles, TimeSpan timeout); public const Int32 WaitTimeout = 258; // WaitAny dan timeout bo'lganda qaytadi // Dispose ichki ravishda Win32 CloseHandle funksiyasini chaqiradi public void Dispose(); }

Bu metodlar haqida e'tibor berish kerak bo'lgan bir necha narsalar:

  • WaitOne — chaqiruvchi threadni asosiy kernel obyekti signallangunicha kutishga majbur qiladi. Ichki ravishda Win32 WaitForSingleObjectEx funksiyasini chaqiradi. Qaytarilgan Boolean — agar obyekt signallangan bo'lsa true, timeout sodir bo'lsa false.
  • WaitAllWaitHandle[] da ko'rsatilgan barcha kernel obyektlari signallanguncha kutadi. Barcha obyektlar signallangan bo'lsa true, timeout bo'lsa false qaytaradi.
  • WaitAnyWaitHandle[] da ko'rsatilgan kernel obyektlaridan istalgan biri signallanguncha kutadi. Qaytarilgan Int32 signallangan kernel obyektning massiv indeksi yoki timeout bo'lsa WaitHandle.WaitTimeout.
  • Dispose — asosiy kernel obyekt handle ni yopadi. Ichki ravishda Win32 CloseHandle funksiyasini chaqiradi. Dispose ni aniq chaqirish o'rniga, garbage collector (GC) tozalashni bajarishiga ruxsat berish tavsiya etiladi.
Eslatma

WaitAny va WaitAll metodlariga uzatadigan massiv 64 tadan ko'p bo'lmagan elementni o'z ichiga olishi kerak, aks holda System.NotSupportedException tashlanadi. Timeout qabul qilmaydigan WaitOne va WaitAll versiyalari void qaytarish turiga ega sifatida prototiplangan, chunki bu metodlar doimo qaytadi va nazarda tutilgan timeout cheksiz (System.Threading.Timeout.Infinite).

Event Konstruktsiyalar

Eventlar oddiyroq qilib aytganda, yadro tomonidan boshqariladigan Boolean o'zgaruvchilaridir. Event da kutayotgan thread event false bo'lganda bloklanadi va event true bo'lganda blokdan chiqadi. Eventlarning ikki turi mavjud:

  • Auto-reset eventtrue bo'lganda, u faqat bitta bloklangan threadni uyg'otadi, chunki yadro birinchi threadni blokdan chiqargandan keyin eventni avtomatik ravishda false ga qaytaradi.
  • Manual-reset eventtrue bo'lganda, u barcha kutayotgan threadlarni blokdan chiqaradi, chunki yadro eventni avtomatik ravishda false ga qaytarmaydi; kodingiz eventni qo'lda false ga qaytarishi kerak.
C# public class EventWaitHandle : WaitHandle { public Boolean Set(); // Boolean ni true ga o'rnatadi; doimo true qaytaradi public Boolean Reset(); // Boolean ni false ga o'rnatadi; doimo true qaytaradi } public sealed class AutoResetEvent : EventWaitHandle { public AutoResetEvent(Boolean initialState); } public sealed class ManualResetEvent : EventWaitHandle { public ManualResetEvent(Boolean initialState); }

Auto-reset event yordamida SimpleSpinLock ga o'xshash xatti-harakatga ega thread sinxronizatsiya qulfini osongina yaratishimiz mumkin:

C# internal sealed class SimpleWaitLock : IDisposable { private readonly AutoResetEvent m_available; public SimpleWaitLock() { m_available = new AutoResetEvent(true); // Dastlab bo'sh } public void Enter() { // Resurs mavjud bo'lgunicha kernel da bloklash m_available.WaitOne(); } public void Leave() { // Boshqa threadga resursga kirishga ruxsat berish m_available.Set(); } public void Dispose() { m_available.Dispose(); } }

Siz bu SimpleWaitLock ni SimpleSpinLock bilan xuddi bir xil tarzda ishlatasiz. Aslida, tashqi xatti-harakat bir xil; biroq, ikkala qulfning unumdorligi tubdan farq qiladi. Qulf uchun tortishuv bo'lmaganda, SimpleWaitLock SimpleSpinLock dan ancha sekin, chunki SimpleWaitLock ning Enter va Leave metodlariga har bir chaqiruv chaqiruvchi threadni managed koddan yadro kodiga va orqaga o'tishga majbur qiladi. Lekin tortishuv bo'lganda, yutqazgan thread bloklangan va CPU sikllarini behuda sarflamaydi — bu yaxshi.

Unumdorlik farqlarini yaxshiroq tushunish uchun, men quyidagi kodni yozdim:

C# public static void Main() { Int32 x = 0; const Int32 iterations = 10000000; // 10 million // x ni 10 million marta oshirish qancha vaqt oladi? Stopwatch sw = Stopwatch.StartNew(); for (Int32 i = 0; i < iterations; i++) { x++; } Console.WriteLine("Incrementing x: {0:N0}", sw.ElapsedMilliseconds); // Hech narsa qilmaydigan metodni chaqirish qo'shilsa? sw.Restart(); for (Int32 i = 0; i < iterations; i++) { M(); x++; M(); } Console.WriteLine("Incrementing x in M: {0:N0}", sw.ElapsedMilliseconds); // Tortishmaydigan SpinLock qo'shilsa? SpinLock sl = new SpinLock(false); sw.Restart(); for (Int32 i = 0; i < iterations; i++) { Boolean taken = false; sl.Enter(ref taken); x++; sl.Exit(); } Console.WriteLine("Incrementing x in SpinLock: {0:N0}", sw.ElapsedMilliseconds); // Tortishmaydigan SimpleWaitLock qo'shilsa? using (SimpleWaitLock swl = new SimpleWaitLock()) { sw.Restart(); for (Int32 i = 0; i < iterations; i++) { swl.Enter(); x++; swl.Leave(); } Console.WriteLine("Incrementing x in SimpleWaitLock: {0:N0}", sw.ElapsedMilliseconds); } } [MethodImpl(MethodImplOptions.NoInlining)] private static void M() { /* Bu metod hech narsa qilmaydi */ }

Natijalar:

OperatsiyaVaqt (ms)Farq
x++ (hech narsasiz)8Eng tez
x++ (bo'sh metodlar bilan)69~9x sekinroq
x++ (SpinLock bilan)164~21x sekinroq
x++ (SimpleWaitLock bilan)8,854~1,107x sekinroq

Ko'rib turganingizdek, shunchaki x ni oshirish faqat 8 millisekund oldi. Bo'sh metodlarni chaqirish operatsiyani to'qqiz marta sekinlashtirdi! User-mode konstruktsiyasini ishlatadigan metod 21 marta sekinroq ishladi (164 / 8). Lekin endi kernel-mode konstruktsiyasini ishlatgan dastur qanchalik sekinroq ishlaganini ko'ring: 1,107 marta (8,854 / 8) sekinroq! Demak, agar thread sinxronizatsiyasidan qochish imkoni bo'lsa, undan qoching. Agar thread sinxronizatsiyasi kerak bo'lsa, user-mode konstruktsiyalarini ishlating. Har doim kernel-mode konstruktsiyalaridan qochishga harakat qiling.

Semaphore Konstruktsiyalar

Semaforlar oddiyroq qilib aytganda, yadro tomonidan boshqariladigan Int32 o'zgaruvchilaridir. Semaforda kutayotgan thread semafor 0 bo'lganda bloklanadi va semafor 0 dan katta bo'lganda blokdan chiqadi. Thread semaforda blokdan chiqarilganda, yadro avtomatik ravishda semaforning hisobidan 1 ni ayiradi. Semaforlarning maksimal Int32 qiymati ham mavjud va joriy hisob hech qachon maksimal hisobdan oshishga ruxsat berilmaydi.

C# public sealed class Semaphore : WaitHandle { public Semaphore(Int32 initialCount, Int32 maximumCount); public Int32 Release(); // Release(1) ni chaqiradi; oldingi hisobni qaytaradi public Int32 Release(Int32 releaseCount); // Oldingi hisobni qaytaradi }

Bu uchta kernel-mode primitivlar qanday xatti-harakat ko'rsatishini umumlashtiray:

  • Bir nechta threadlar auto-reset event da kutayotgan bo'lsa, eventni o'rnatish faqat bitta threadni blokdan chiqaradi.
  • Bir nechta threadlar manual-reset event da kutayotgan bo'lsa, eventni o'rnatish barcha threadlarni blokdan chiqaradi.
  • Bir nechta threadlar semaforda kutayotgan bo'lsa, semaforni bo'shatish releaseCount ta threadni blokdan chiqaradi (bu Semaphore.Release metodiga uzatilgan argument).

Shuning uchun, auto-reset event maksimal hisob 1 bo'lgan semaforga juda o'xshash xatti-harakat ko'rsatadi. Farq shundaki, auto-reset event da Set ketma-ket bir necha marta chaqirilishi mumkin va faqat bitta thread blokdan chiqadi, semaford esa Release ni ketma-ket chaqirish ichki hisobni oshirib boradi va ko'p threadlarni blokdan chiqarishi mumkin. Agar semaforga Release ni juda ko'p marta chaqirsangiz va hisobi maksimal hisobdan oshib ketsa, SemaphoreFullException tashlanadi.

Semafor yordamida SimpleWaitLock ni qayta amalga oshirib, bir nechta threadlarga resursga bir vaqtda kirishga ruxsat berish mumkin (bu barcha threadlar resursga faqat o'qish rejimida kiradigan bo'lmasa, xavfsiz bo'lmasligi mumkin):

C# public sealed class SimpleWaitLock : IDisposable { private readonly Semaphore m_available; public SimpleWaitLock(Int32 maxConcurrent) { m_available = new Semaphore(maxConcurrent, maxConcurrent); } public void Enter() { // Resurs mavjud bo'lgunicha kernel da bloklash m_available.WaitOne(); } public void Leave() { // Boshqa threadga resursga kirishga ruxsat berish m_available.Release(1); } public void Dispose() { m_available.Close(); } }

Kernel-mode konstruktsiyalarining keng tarqalgan foydalanish usullaridan biri — bir vaqtda o'zining faqat bitta nusxasi bajarilishiga ruxsat beradigan ilova yaratishdir. Yagona nusxa ilovalariga misollar: Microsoft Outlook, Windows Media Player va boshqalar. Mana yagona nusxa ilovani qanday amalga oshirish kerak:

C# using System; using System.Threading; public static class Program { public static void Main() { Boolean createdNew; // Belgilangan nom bilan kernel obyekti yaratishga urinish using (new Semaphore(0, 1, "SomeUniqueStringIdentifyingMyApp", out createdNew)) { if (createdNew) { // Bu thread kernel obyektini yaratdi, shuning uchun bu ilovaning // boshqa nusxasi ishlamayapti. Ilovaning qolgan qismini shu yerda ishga tushiring... } else { // Bu thread xuddi shu satr nomi bilan mavjud kernel // obyektini ochdi; bu ilovaning boshqa nusxasi ishlamoqda. // Bu yerda qilish kerak narsa yo'q, Main dan qaytib chiqib, // bu ikkinchi nusxani tugatamiz. } } } }

Bu kodda men Semaphore ishlatmoqdaman, lekin EventWaitHandle yoki Mutex ishlatganimda ham ishlaydi, chunki men thread sinxronizatsiya xatti-harakatidan emas, yadro har qanday turdagi kernel obyektini yaratishda taklif qiladigan thread sinxronizatsiya xatti-harakatidan foydalanmoqdaman. Yadro belgilangan nom bilan faqat bitta kernel obyekti yaratilishini ta'minlaydi; obyektni yaratgan threadning createdNew o'zgaruvchisi true ga o'rnatiladi. Ikkinchi thread uchun Windows allaqachon mavjud kernel obyektini ko'radi va createdNew false ga o'rnatiladi.

Mutex Konstruktsiyalar

Mutex o'zaro istisno (mutual-exclusive) qulfni ifodalaydi. U AutoResetEvent yoki hisob 1 bo'lgan Semaphore ga o'xshash ishlaydi, chunki uchala konstruktsiya ham bir vaqtda faqat bitta kutayotgan threadni bo'shatadi.

C# public sealed class Mutex : WaitHandle { public Mutex(); public void ReleaseMutex(); }

Mutexlarda qo'shimcha mantiq mavjud bo'lib, bu ularni boshqa konstruktsiyalarga qaraganda murakkabroq qiladi:

  • Thread egaligini kuzatish: Mutex obyektlari qaysi thread uni olganini chaqiruvchi threadning Int32 ID sini so'rash orqali yozib oladi. Thread ReleaseMutex ni chaqirganda, Mutex chaqiruvchi threadning Mutex ni olgan xuddi shu thread ekanligini tekshiradi. Agar chaqiruvchi thread Mutex ni olgan thread bo'lmasa, Mutex ning holati o'zgarmaydi va ReleaseMutex System.ApplicationException tashlaydi. Shuningdek, agar Mutex ga ega bo'lgan thread boshqa sabab bilan tugatilsa, Mutex da kutayotgan thread System.Threading.AbandonedMutexException tashlanishi bilan uyg'otiladi.
  • Rekursiya hisobi: Mutex obyektlari egasi threadning Mutex ga necha marta ega ekanligini ko'rsatadigan rekursiya hisobini yuritadi. Agar thread hozirda Mutex ga ega bo'lsa va keyin u shu Mutex da yana kutsa, rekursiya hisob oshiriladi va threadga davom etishga ruxsat beriladi. Thread ReleaseMutex ni chaqirganda, rekursiya hisob kamaytiriladi. Faqat rekursiya hisob 0 ga yetganda boshqa thread Mutex ning egasi bo'lishi mumkin.
Mutex dan qochish tavsiya etiladi

Ko'p odamlar bu qo'shimcha mantiqni yoqtirmaydilar. Muammo shundaki, bu "xususiyatlar"ning narxi bor. Mutex obyekti qo'shimcha thread ID va rekursiya hisob ma'lumotlarini saqlash uchun ko'proq xotira talab qiladi. Bundan ham muhimi, Mutex kodi bu ma'lumotni yuritishi kerak, bu qulfni sekinlashtiradi. Shu sababli, ko'p odamlar Mutex obyektlaridan qochishadi.

Odatda, rekursiv qulf metod qulf olganida va keyin shuningdek qulf talab qiladigan boshqa metodni chaqirganida kerak bo'ladi:

C# internal class SomeClass : IDisposable { private readonly Mutex m_lock = new Mutex(); public void Method1() { m_lock.WaitOne(); // Biror ish bajarish... Method2(); // Method2 rekursiv ravishda qulfni oladi m_lock.ReleaseMutex(); } public void Method2() { m_lock.WaitOne(); // Biror ish bajarish... m_lock.ReleaseMutex(); } public void Dispose() { m_lock.Dispose(); } }

Oldingi kodda SomeClass obyektidan foydalanadigan kod Method1 ni chaqirishi mumkin, u Mutex ni oladi, ba'zi thread-safe operatsiyalarni bajaradi va keyin xuddi shu Method2 ni chaqiradi. Mutex obyektlari rekursiyani qo'llab-quvvatlagani uchun, thread qulfni ikki marta oladi va keyin boshqa thread Mutex ga ega bo'lishidan oldin uni ikki marta bo'shatadi. Agar SomeClass Mutex o'rniga AutoResetEvent ishlatganda, thread Method2 ning WaitOne ni chaqirganda bloklanardi.

Agar sizga rekursiv qulf kerak bo'lsa, AutoResetEvent dan foydalanib osongina yaratishingiz mumkin:

C# internal sealed class RecursiveAutoResetEvent : IDisposable { private AutoResetEvent m_lock = new AutoResetEvent(true); private Int32 m_owningThreadId = 0; private Int32 m_recursionCount = 0; public void Enter() { // Chaqiruvchi threadning noyob Int32 ID sini oling Int32 currentThreadId = Thread.CurrentThread.ManagedThreadId; // Agar chaqiruvchi thread qulfga ega bo'lsa, rekursiya hisobini oshiring if (m_owningThreadId == currentThreadId) { m_recursionCount++; return; } // Chaqiruvchi thread qulfga ega emas, uni kuting m_lock.WaitOne(); // Chaqiruvchi endi qulfga ega, egasi thread ID va rekursiya hisobini ishga tushiring m_owningThreadId = currentThreadId; m_recursionCount = 1; } public void Leave() { // Agar chaqiruvchi thread qulfga ega bo'lmasa, xato bor if (m_owningThreadId != Thread.CurrentThread.ManagedThreadId) throw new InvalidOperationException(); // Rekursiya hisobidan 1 ni ayirish if (--m_recursionCount == 0) { // Agar rekursiya hisob 0 bo'lsa, hech qanday thread qulfga ega emas m_owningThreadId = 0; m_lock.Set(); // 1 ta kutayotgan threadni uyg'otish (agar bor bo'lsa) } } public void Dispose() { m_lock.Dispose(); } }

RecursiveAutoResetEvent klassining xatti-harakati Mutex klassnikiga o'xshash bo'lsa-da, RecursiveAutoResetEvent obyekti thread rekursiv ravishda qulfni olishga uringanda ancha yaxshi unumdorlikka ega bo'ladi, chunki thread egaligini va rekursiyani kuzatish uchun zarur bo'lgan barcha kod endi managed kodda. Thread Windows yadrosiga faqat AutoResetEvent ni birinchi marta olganida yoki oxir-oqibat boshqa threadga bo'shatganida o'tishi kerak.