.NET: Ko'p oqimli va asinxroniya bilan ishlash uchun asboblar. 1-qism

Men Habr-dagi asl maqolani nashr etyapman, uning tarjimasi korporativda joylashtirilgan blog.

Bu yerda va hozir natijani kutmasdan, biror narsani asinxron qilish yoki uni bajaruvchi bir nechta birliklar orasida katta ishni taqsimlash zarurati kompyuterlar paydo bo'lishidan oldin mavjud edi. Ularning paydo bo'lishi bilan bu ehtiyoj juda aniq bo'ldi. Endi, 2019 yilda men ushbu maqolani 8 yadroli Intel Core protsessoriga ega noutbukda yozyapman, unda yuzdan ortiq jarayonlar parallel ravishda ishlaydi va undan ham ko'proq iplar. Yaqin atrofda bir necha yil oldin sotib olingan biroz eskirgan telefon bor, bortida 8 yadroli protsessor bor. Tematik manbalar maqolalar va videolar bilan to'la bo'lib, ularning mualliflari 16 yadroli protsessorlarga ega bu yilgi flagman smartfonlariga qoyil qolishadi. MS Azure 20 yadroli protsessor va 128 TB operativ xotiraga ega virtual mashinani soatiga 2 dollardan kamroq vaqtga taqdim etadi. Afsuski, iplarning o'zaro ta'sirini boshqara olmasdan, maksimal quvvatni olish va bu quvvatdan foydalanish mumkin emas.

Terminologiya

Jarayon - OS ob'ekti, ajratilgan manzil maydoni, iplarni o'z ichiga oladi.
Ip - OT ob'ekti, bajarilishning eng kichik birligi, jarayonning bir qismi, oqimlar jarayon doirasida o'zaro xotira va boshqa resurslarni taqsimlaydi.
Ko'p ishlarni bajarish - OS xususiyati, bir vaqtning o'zida bir nechta jarayonlarni bajarish qobiliyati
Ko'p yadroli - protsessorning xususiyati, ma'lumotlarni qayta ishlash uchun bir nechta yadrolardan foydalanish qobiliyati
Ko'p ishlov berish - kompyuterning xususiyati, bir vaqtning o'zida bir nechta protsessorlar bilan jismoniy ishlash qobiliyati
Ko'p tarmoqli - jarayonning xossasi, ma'lumotlarni qayta ishlashni bir nechta iplar orasida taqsimlash qobiliyati.
Parallellik - vaqt birligida jismoniy bir vaqtning o'zida bir nechta harakatlarni bajarish
Asinxroniya — ushbu ishlov berish tugashini kutmasdan operatsiyani bajarish, bajarish natijasi keyinroq qayta ishlanishi mumkin;

Metafora

Hamma ta'riflar yaxshi emas va ba'zilari qo'shimcha tushuntirishga muhtoj, shuning uchun men rasmiy ravishda kiritilgan terminologiyaga nonushta tayyorlash haqida metafora qo'shaman. Ushbu metaforada nonushta tayyorlash - bu jarayon.

Ertalab nonushta tayyorlayotganimda (Markaziy protsessor) Men oshxonaga keldim (Kompyuter). Mening 2 qo'lim bor (Ranglar). Oshxonada bir qancha qurilmalar mavjud (IO): pech, choynak, tushdi mashinasi, muzlatgich. Men gazni yoqaman, ustiga tovani qo'yaman va qizishini kutmasdan yog'ni quyaman (asinxron, Non-Blocking-IO-Wait), Men tuxumni muzlatgichdan olib, plastinkaga sindiraman, keyin ularni bir qo'lim bilan uraman (№1 mavzu) va ikkinchi (№2 mavzu) plastinkani ushlab turish (Birgalikda Resurs). Endi men choynakni yoqmoqchiman, lekin qo'llarim etishmayapti (Ip ochligi) Bu vaqt ichida qavrilgan idish qiziydi (Natijani qayta ishlash), men qamchilagan narsamni quyaman. Men choynakga qo'l uzataman va uni yoqaman va unda qaynayotgan suvni ahmoqona tomosha qilaman (Bloklash-IO-Kuting), garchi bu vaqt ichida u omletni qamchilagan plastinkani yuvishi mumkin edi.

Men omletni atigi 2 qo'l bilan pishirdim, ko'pi yo'q, lekin shu bilan birga, omletni qamchilash vaqtida birdaniga 3 ta operatsiya amalga oshirildi: omletni qamchilash, likopchani ushlab turish, tovani qizdirish Protsessor kompyuterning eng tezkor qismidir, IO ko'pincha hamma narsa sekinlashadi, shuning uchun ko'pincha IO'dan ma'lumotlarni qabul qilishda protsessorni biror narsa bilan band qilish samarali echimdir.

Metaforani davom ettirish:

  • Agar omlet tayyorlash jarayonida men ham kiyimni almashtirishga harakat qilsam, bu ko'p vazifani bajarishga misol bo'lardi. Muhim nuance: kompyuterlar bu borada odamlarga qaraganda ancha yaxshi.
  • Bir nechta oshpazlar bo'lgan oshxona, masalan, restoranda - ko'p yadroli kompyuter.
  • Savdo markazidagi oziq-ovqat kortidagi ko'plab restoranlar - ma'lumotlar markazi

.NET vositalari

.NET ko'plab boshqa narsalar kabi iplar bilan ishlashda yaxshi. Har bir yangi versiya bilan u ular bilan ishlash uchun tobora ko'proq yangi vositalarni, OS iplari bo'yicha yangi abstraktsiya qatlamlarini taqdim etadi. Abstraktsiyalar konstruktsiyasi bilan ishlashda ramka ishlab chiquvchilari yuqori darajadagi abstraktsiyadan foydalanganda bir yoki bir necha daraja pastga tushish imkoniyatini qoldiradigan yondashuvdan foydalanadilar. Ko'pincha bu kerak emas, aslida bu o'z-o'zidan ov miltig'i bilan oyog'ingizga otish uchun eshikni ochadi, lekin ba'zida, kamdan-kam hollarda, hozirgi mavhumlik darajasida hal etilmagan muammoni hal qilishning yagona yo'li bo'lishi mumkin. .

Asboblar deganda men ramka va uchinchi tomon paketlari tomonidan taqdim etilgan amaliy dasturlash interfeyslarini (API), shuningdek, ko'p bosqichli kod bilan bog'liq har qanday muammolarni qidirishni soddalashtiradigan butun dasturiy echimlarni nazarda tutyapman.

Mavzuni boshlash

Thread klassi iplar bilan ishlash uchun .NET da eng asosiy sinf hisoblanadi. Konstruktor ikkita delegatdan birini qabul qiladi:

  • ThreadStart - Parametrlar yo'q
  • ParametrizedThreadStart - turdagi ob'ektning bitta parametri bilan.

Delegat Start usulini chaqirgandan so'ng yangi yaratilgan ish zarrachasida bajariladi. Agar konstruktorga ParametrizedThreadStart tipidagi delegat uzatilgan bo'lsa, u holda ob'ekt Start usuliga o'tkazilishi kerak. Ushbu mexanizm har qanday mahalliy ma'lumotni oqimga o'tkazish uchun kerak. Ta'kidlash joizki, ip yaratish qimmat operatsiya hisoblanadi va ipning o'zi og'ir ob'ektdir, chunki u stekda 1 MB xotirani ajratadi va OS API bilan o'zaro aloqani talab qiladi.

new Thread(...).Start(...);

ThreadPool klassi hovuz tushunchasini ifodalaydi. .NET-da iplar hovuzi muhandislikning bir qismidir va Microsoft-dagi ishlab chiquvchilar uning turli xil stsenariylarda optimal ishlashiga ishonch hosil qilish uchun juda ko'p kuch sarfladilar.

Umumiy tushuncha:

Ilova boshlangan paytdan boshlab, u fonda zaxirada bir nechta iplarni yaratadi va ularni foydalanish uchun olish imkoniyatini beradi. Agar iplar tez-tez va ko'p miqdorda ishlatilsa, hovuz qo'ng'iroq qiluvchining ehtiyojlarini qondirish uchun kengayadi. To'g'ri vaqtda hovuzda bo'sh iplar bo'lmasa, u yo iplardan birining qaytishini kutadi yoki yangisini yaratadi. Bundan kelib chiqadiki, iplar hovuzi ba'zi qisqa muddatli harakatlar uchun juda yaxshi va dasturning butun faoliyati davomida xizmat sifatida ishlaydigan operatsiyalar uchun juda mos emas.

Hovuzdan ipni ishlatish uchun ParametrizedThreadStart bilan bir xil imzoga ega bo'lgan WaitCallback tipidagi delegatni qabul qiluvchi QueueUserWorkItem usuli mavjud va unga uzatilgan parametr bir xil funktsiyani bajaradi.

ThreadPool.QueueUserWorkItem(...);

Bloklanmagan IO operatsiyalarini tashkil qilish uchun RegisterWaitForSingleObject kamroq ma'lum bo'lgan iplar puli usuli qo'llaniladi. Ushbu usulga o'tgan delegat usulga o'tkazilgan WaitHandle "Bo'shatilgan" bo'lganda chaqiriladi.

ThreadPool.RegisterWaitForSingleObject(...)

.NET da ip taymeri mavjud va u WinForms/WPF taymerlaridan farq qiladi, chunki uning ishlov beruvchisi hovuzdan olingan ipda chaqiriladi.

System.Threading.Timer

Havuzdagi ipga delegatni bajarish uchun yuborishning juda ekzotik usuli ham mavjud - BeginInvoke usuli.

DelegateInstance.BeginInvoke

Men yuqoridagi usullarning ko'pini chaqirish mumkin bo'lgan funksiya haqida qisqacha to'xtalib o'tmoqchiman - Kernel32.dll Win32 API-dan CreateThread. Ekstern usullar mexanizmi tufayli bu funktsiyani chaqirishning bir usuli bor. Men bunday qo'ng'iroqni faqat bir marta meros kodining dahshatli misolida ko'rganman va aynan shunday qilgan muallifning motivatsiyasi men uchun haligacha sir bo'lib qolmoqda.

Kernel32.dll CreateThread

Mavzularni ko'rish va disk raskadrovka

Siz yaratgan mavzularni, barcha uchinchi tomon komponentlarini va .NET pulini Visual Studio ning Threads oynasida ko'rish mumkin. Ushbu oyna faqat dastur disk raskadrovka rejimida va Break rejimida bo'lganda mavzu ma'lumotlarini ko'rsatadi. Bu yerda siz har bir ipning stek nomlari va ustuvorliklarini qulay tarzda ko'rishingiz va disk raskadrovkani ma'lum bir mavzuga almashtirishingiz mumkin. Thread sinfining Priority xususiyatidan foydalanib, OC va CLR protsessor vaqtini iplar o'rtasida taqsimlashda tavsiya sifatida qabul qiladigan ipning ustuvorligini belgilashingiz mumkin.

.NET: Ko'p oqimli va asinxroniya bilan ishlash uchun asboblar. 1-qism

Parallel kutubxona vazifasi

Task Parallel Library (TPL) .NET 4.0 da joriy qilingan. Endi u asinxroniya bilan ishlash uchun standart va asosiy vositadir. Eski yondashuvdan foydalanadigan har qanday kod eski hisoblanadi. TPL ning asosiy birligi System.Threading.Tasks nom maydonidagi Task sinfidir. Vazifa - bu ip ustidagi mavhumlik. C# tilining yangi versiyasi bilan biz Tasks bilan ishlashning nafis usuliga ega bo'ldik - async/await operatorlari. Ushbu tushunchalar asinxron kodni xuddi oddiy va sinxron bo'lganidek yozishga imkon berdi, bu hatto iplarning ichki ishlashini kam tushunadigan odamlarga ham ulardan foydalanadigan ilovalarni, uzoq operatsiyalarni bajarishda qotib qolmaydigan ilovalarni yozishga imkon berdi. Async/await dan foydalanish bir yoki hatto bir nechta maqolalar uchun mavzudir, lekin men uning mohiyatini bir necha jumlada tushunishga harakat qilaman:

  • async - bu Task yoki voidni qaytaruvchi usulning modifikatoridir
  • va kutish - bloklanmaydigan Vazifani kutish operatori.

Yana bir bor: kutish operatori, umumiy holatda (istisnolar mavjud) joriy ijro chizig'ini qo'shimcha ravishda chiqaradi va Vazifa o'z bajarilishini tugatgandan so'ng, ipni (aslida, kontekstni aytish to'g'ri bo'lar edi). , lekin bu haqda keyinroq) usulni davom ettirishni davom ettiradi. .NET ichida bu mexanizm xuddi yield return bilan bir xil tarzda amalga oshiriladi, qachonki yozma usul butun sinfga aylanganda, davlat mashinasi bo'lib, bu holatlarga qarab alohida qismlarda bajarilishi mumkin. Har bir qiziquvchi har qanday oddiy kodni asyns/await yordamida yozishi, kompilyatsiya qilish va kompilyator tomonidan yaratilgan kod yoqilgan JetBrains dotPeek yordamida yig'ilishni ko'rishi mumkin.

Keling, Vazifani ishga tushirish va ishlatish variantlarini ko'rib chiqaylik. Quyidagi kod misolida biz hech qanday foydali ish qilmaydigan yangi vazifa yaratamiz (Thread.Sleep (10000)), lekin haqiqiy hayotda bu murakkab protsessor talab qiladigan ish bo'lishi kerak.

using TCO = System.Threading.Tasks.TaskCreationOptions;

public static async void VoidAsyncMethod() {
    var cancellationSource = new CancellationTokenSource();

    await Task.Factory.StartNew(
        // Code of action will be executed on other context
        () => Thread.Sleep(10000),
        cancellationSource.Token,
        TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
        scheduler
    );

    //  Code after await will be executed on captured context
}

Vazifa bir nechta variantlar bilan yaratiladi:

  • LongRunning - bu vazifa tezda bajarilmasligiga ishora, ya'ni hovuzdan ipni olmaslik, boshqalarga zarar bermaslik uchun bu vazifa uchun alohida yaratish haqida o'ylash arziydi.
  • AttachedToParent - Vazifalar ierarxiyada tartibga solinishi mumkin. Agar ushbu parametr ishlatilgan bo'lsa, u holda Vazifa o'zi tugatgan va bolalarining bajarilishini kutayotgan holatda bo'lishi mumkin.
  • PreferFairness - oldinroq yuborilgan topshiriqlarni keyinroq yuborilganidan oldin bajarish yaxshiroq bo'lishini anglatadi. Ammo bu faqat tavsiya va natijalar kafolatlanmaydi.

Usulga uzatilgan ikkinchi parametr CancellationToken hisoblanadi. Amaliyot boshlanganidan keyin bekor qilishni to'g'ri bajarish uchun bajarilayotgan kod CancellationToken holatini tekshirish bilan to'ldirilishi kerak. Agar tekshiruvlar bo'lmasa, CancellationTokenSource ob'ektida chaqirilgan Cancel usuli Vazifaning bajarilishini faqat boshlanishidan oldin to'xtata oladi.

Oxirgi parametr TaskScheduler tipidagi rejalashtiruvchi ob'ektdir. Bu sinf va uning avlodlari, sukut bo'yicha, Vazifalarni iplar bo'ylab tarqatish strategiyalarini boshqarish uchun mo'ljallangan, Vazifa hovuzdan tasodifiy ipda bajariladi.

Yaratilgan vazifaga kutish operatori qo'llaniladi, ya'ni undan keyin yozilgan kod, agar mavjud bo'lsa, kutishdan oldingi kod bilan bir xil kontekstda (ko'pincha bu bir xil mavzuda) bajariladi.

Usul async void sifatida belgilangan, ya'ni u kutish operatoridan foydalanishi mumkin, ammo chaqiruv kodi bajarilishini kuta olmaydi. Agar bunday xususiyat zarur bo'lsa, usul Vazifani qaytarishi kerak. Asenkron bo'lmagan deb belgilangan usullar juda keng tarqalgan: qoida tariqasida, bu hodisa ishlov beruvchilari yoki yong'in va unutish printsipida ishlaydigan boshqa usullar. Agar siz nafaqat bajarilishning oxirigacha kutish imkoniyatini berishingiz, balki natijani qaytarishingiz kerak bo'lsa, unda siz Task-dan foydalanishingiz kerak.

StartNew usuli qaytargan vazifada va boshqa har qanday usulda siz noto'g'ri parametr bilan ConfigureAwait usulini chaqirishingiz mumkin, keyin kutishdan keyin bajarish olingan kontekstda emas, balki o'zboshimchalik bilan davom etadi. Buni har doim kutishdan keyin bajarish konteksti kod uchun muhim bo'lmaganda qilish kerak. Bu, shuningdek, kutubxonaga qadoqlangan holda yetkaziladigan kod yozishda MS tomonidan tavsiya etilgan.

Vazifaning bajarilishini qanday kutish mumkinligi haqida bir oz ko'proq to'xtalib o'tamiz. Quyida kutish shartli ravishda yaxshi bajarilganda va shartli ravishda yomon bajarilganda sharhlar bilan kod misoli keltirilgan.

public static async void AnotherMethod() {

    int result = await AsyncMethod(); // good

    result = AsyncMethod().Result; // bad

    AsyncMethod().Wait(); // bad

    IEnumerable<Task> tasks = new Task[] {
        AsyncMethod(), OtherAsyncMethod()
    };

    await Task.WhenAll(tasks); // good
    await Task.WhenAny(tasks); // good

    Task.WaitAll(tasks.ToArray()); // bad
}

Birinchi misolda, biz qo'ng'iroq qiluvchi ipni bloklamasdan, vazifani bajarishni kutamiz, biz natijani faqat u erda bo'lganda qayta ishlashga qaytamiz, chaqiruvchi ip o'z qurilmalarida qoladi;

Ikkinchi variantda usulning natijasi hisoblanmaguncha chaqiruvchi ipni bloklaymiz. Bu yomon, chunki biz dasturning bunday qimmatli manbasi bo'lgan mavzuni oddiy bekorchilik bilan egallab olganimiz uchun emas, balki biz chaqiradigan usulning kodi kutishni o'z ichiga olgan bo'lsa va sinxronizatsiya konteksti qo'ng'iroq qiluvchi mavzuga qaytishni talab qilsa, buning sababi ham. kuting, keyin biz boshi berk ko'chaga tushamiz : Asinxron usulning natijasi hisoblanayotganda chaqiruvchi ip kutadi, asinxron usul chaqiruvchi ipda o'z bajarilishini davom ettirish uchun behuda harakat qiladi.

Ushbu yondashuvning yana bir kamchiligi - bu xatolar bilan ishlashning murakkabligi. Gap shundaki, async/await-dan foydalanishda asinxron koddagi xatolarni hal qilish juda oson - ular xuddi kod sinxron bo'lgani kabi ishlaydi. Vazifaga sinxron kutish ekzorsizmini qo'llasak, asl istisno AggregateException ga aylanadi, ya'ni. Istisnoni hal qilish uchun siz InnerException turini ko'rib chiqishingiz va C# dunyosida ko'proq tanish bo'lgan catch bloklari zanjiri o'rniga bitta catch blokiga o'zingiz if zanjirini yozishingiz yoki qurishda catch dan foydalanishingiz kerak bo'ladi.

Uchinchi va oxirgi misollar ham xuddi shu sababga ko'ra yomon deb belgilangan va barchasi bir xil muammolarni o'z ichiga oladi.

WhenAny va WhenAll usullari bir guruh vazifalarni kutish uchun juda qulaydir, ular bir guruh Vazifalarni birlashtiradi, ular guruhdagi topshiriq birinchi marta ishga tushirilganda yoki ularning barchasi bajarilganda ishga tushadi.

Iplarni to'xtatish

Turli sabablarga ko'ra, oqim boshlanganidan keyin uni to'xtatish kerak bo'lishi mumkin. Buning bir qancha usullari mavjud. Thread klassi ikkita mos ravishda nomlangan usulga ega: Abort qilish и To'xtatish. Birinchisini ishlatish tavsiya etilmaydi, chunki har qanday tasodifiy daqiqada uni chaqirgandan so'ng, har qanday ko'rsatmani qayta ishlash paytida istisno chiqariladi ThreadAbortedException. Har qanday tamsayı o'zgaruvchisini oshirishda bunday istisno paydo bo'lishini kutmaysiz, to'g'rimi? Va bu usuldan foydalanganda, bu juda haqiqiy holat. Agar siz kodning ma'lum bir qismida CLR-ning bunday istisnoni yaratishiga yo'l qo'ymaslik kerak bo'lsa, uni qo'ng'iroqlarga o'rashingiz mumkin. Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Finally blokida yozilgan har qanday kod bunday qo'ng'iroqlarga o'raladi. Shu sababli, ramka kodining chuqurligida siz bo'sh urinish bilan bloklarni topishingiz mumkin, lekin nihoyat bo'sh emas. Microsoft bu usulni shunchalik rad etadiki, ular uni .net yadrosiga kiritmagan.

Interrupt usuli ko'proq taxminiy ishlaydi. Istisno bilan ipni to'xtatishi mumkin ThreadInterruptedException faqat ip kutish holatida bo'lgan paytlarda. Bu holatga WaitHandle, qulflash yoki Thread.Sleep qo'ng'iroq qilgandan keyin osilgan holda kiradi.

Yuqorida tavsiflangan ikkala variant ham oldindan aytib bo'lmaydiganligi sababli yomon. Yechim strukturani ishlatishdir CancellationToken va sinf CancellationTokenSource. Gap shundaki: CancellationTokenSource sinfining namunasi yaratiladi va faqat unga ega bo'lgan kishi usulni chaqirish orqali operatsiyani to'xtatishi mumkin. Bekor qilish. Faqat CancellationToken operatsiyaning o'ziga uzatiladi. CancellationToken egalari operatsiyani o‘zlari bekor qila olmaydi, faqat operatsiya bekor qilinganligini tekshirishi mumkin. Buning uchun mantiqiy xususiyat mavjud IsCancellationRequested va usul ThrowIfCancelRequested. Ikkinchisi istisno qiladi TaskCancelledException agar CancellationToken to'tiqush qilingan misolida Cancel usuli chaqirilgan bo'lsa. Va bu men foydalanishni tavsiya qiladigan usul. Bu istisno operatsiyasini qaysi nuqtada to'xtatish mumkinligini to'liq nazorat qilish orqali oldingi variantlarga nisbatan yaxshilanishdir.

Mavzuni to'xtatishning eng shafqatsiz varianti Win32 API TerminateThread funktsiyasini chaqirishdir. Ushbu funktsiyani chaqirgandan keyin CLR ning xatti-harakati oldindan aytib bo'lmaydigan bo'lishi mumkin. MSDN-da ushbu funktsiya haqida quyidagilar yozilgan: “TerminateThread xavfli funksiya bo‘lib, uni faqat o‘ta og‘ir holatlarda ishlatish kerak. "

FromAsync usuli yordamida eski API-ni Task Based-ga aylantirish

Agar sizga Tasks taqdim etilgandan keyin boshlangan loyiha ustida ishlash omadingiz bo'lsa va ko'pchilik ishlab chiquvchilar uchun sokin dahshatga sabab bo'lishni to'xtatgan bo'lsa, unda siz ko'plab eski API-lar, ham uchinchi tomon, ham sizning jamoangiz bilan shug'ullanishingiz shart emas. o'tmishda qiynoqqa solgan. Yaxshiyamki, .NET Framework jamoasi bizga g'amxo'rlik qildi, garchi maqsad o'zimizga g'amxo'rlik qilish edi. Qanday bo'lmasin, .NET da eski asinxron dasturlash yondashuvlarida yozilgan kodni yangisiga og'riqsiz aylantirish uchun bir qator vositalar mavjud. Ulardan biri TaskFactory ning FromAsync usulidir. Quyidagi kod misolida men ushbu usuldan foydalangan holda WebRequest sinfining eski asinxron usullarini Vazifaga o'tkazaman.

object state = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
    wr.BeginGetResponse,
    we.EndGetResponse
);

Bu shunchaki misol va siz buni o'rnatilgan turlar bilan bajarishingiz dargumon, lekin har qanday eski loyiha IAsyncResult va EndDoSomething usullarini qaytaradigan BeginDoSomething usullari bilan to'ldirilgan.

TaskCompletionSource sinfidan foydalanib, eski API-ni Task Asosga aylantiring

Ko'rib chiqilishi kerak bo'lgan yana bir muhim vosita - bu sinf Vazifani yakunlash manbasi. Funktsiyalari, maqsadi va ishlash printsipi nuqtai nazaridan, men yuqorida yozgan ThreadPool sinfining RegisterWaitForSingleObject usulini biroz eslatishi mumkin. Ushbu sinfdan foydalanib, siz eski asinxron API-larni Tasks-ga osongina va qulay tarzda o'rashingiz mumkin.

Siz aytasizki, men bu maqsadlar uchun mo'ljallangan TaskFactory sinfining FromAsync usuli haqida gapirganman. Bu erda biz Microsoft so'nggi 15 yil ichida taqdim etgan .net-da asinxron modellarning rivojlanishining butun tarixini esga olishimiz kerak bo'ladi: Task-Based Asinxron Pattern (TAP) dan oldin Asinxron Programming Pattern (APP) mavjud edi. usullari haqida edi BoshlashDoSomething qaytmoqda IAsyncResult va usullari OxiriDoSomething buni qabul qiladi va bu yillar merosi uchun FromAsync usuli juda zo'r, ammo vaqt o'tishi bilan uning o'rnini Voqealarga asoslangan asinxron naqsh (VA AP), bu asinxron operatsiya tugagach, voqea ko'tariladi deb faraz qilingan.

TaskCompletionSource Voqealar modeli atrofida qurilgan vazifalar va eski API'larni o'rash uchun juda mos keladi. Uning ishining mohiyati quyidagicha: bu sinf obyekti Task tipidagi umumiy xususiyatga ega, uning holati TaskCompletionSource sinfining SetResult, SetException va hokazo usullari orqali boshqarilishi mumkin. Ushbu vazifaga kutish operatori qo'llanilgan joylarda, TaskCompletionSource-ga qo'llaniladigan usulga qarab, u bajariladi yoki bajarilmaydi. Agar bu hali ham tushunarsiz bo'lsa, keling, ushbu kod misolini ko'rib chiqaylik, bu erda ba'zi eski EAP API TaskCompletionSource yordamida Vazifaga o'ralgan: hodisa boshlanganda, Vazifa Bajarilgan holatiga joylashtiriladi va kutish operatorini qo'llagan usul ushbu Vazifa ob'ektni olgandan so'ng o'z bajarilishini davom ettiradi Natijalar.

public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {

    var completionSource = new TaskCompletionSource<Result>();
    someApiObj.Done += 
        result => completionSource.SetResult(result);
    someApiObj.Do();

    result completionSource.Task;
}

Vazifani bajarish Manba bo'yicha maslahatlar va fokuslar

Eski API-larni o'rash TaskCompletionSource yordamida amalga oshirilishi mumkin bo'lgan hamma narsa emas. Ushbu sinfdan foydalanish mavzularni egallamaydigan vazifalarda turli xil API-larni loyihalashning qiziqarli imkoniyatlarini ochadi. Va oqim, biz eslaganimizdek, qimmat manba va ularning soni cheklangan (asosan RAM miqdori bilan). Ushbu cheklovga, masalan, murakkab biznes mantiqiga ega yuklangan veb-ilovani ishlab chiqish orqali osongina erishish mumkin. Keling, Long-Polling kabi hiylani amalga oshirishda men aytayotgan imkoniyatlarni ko'rib chiqaylik.

Xulosa qilib aytganda, hiylaning mohiyati shundan iboratki, siz API dan uning tomonida sodir bo'layotgan ba'zi hodisalar haqida ma'lumot olishingiz kerak, API esa ba'zi sabablarga ko'ra voqea haqida xabar bera olmaydi, faqat holatni qaytarishi mumkin. Bunga misol qilib, WebSocket davridan oldin yoki biron sababga ko'ra ushbu texnologiyadan foydalanish imkoni bo'lmaganda HTTP ustiga qurilgan barcha APIlarni keltirish mumkin. Mijoz HTTP serveridan so'rashi mumkin. HTTP serveri mijoz bilan aloqani o'zi boshlay olmaydi. Oddiy yechim bu serverda taymer yordamida so'rov o'tkazishdir, lekin bu serverda qo'shimcha yuk va o'rtacha TimerInterval / 2 qo'shimcha kechikish hosil qiladi. Buni hal qilish uchun Long Polling deb nomlangan hiyla ixtiro qilindi, u javobni kechiktirishni o'z ichiga oladi. Vaqt tugashi yoki voqea sodir bo'lguncha server. Agar voqea sodir bo'lgan bo'lsa, u qayta ishlanadi, agar bo'lmasa, so'rov yana yuboriladi.

while(!eventOccures && !timeoutExceeded)  {

  CheckTimout();
  CheckEvent();
  Thread.Sleep(1);
}

Ammo tadbirni kutayotgan mijozlar soni ortishi bilan bunday yechim dahshatli bo'ladi, chunki... Har bir bunday mijoz voqeani kutayotgan butun ipni egallaydi. Ha, va hodisa boshlanganda biz qo'shimcha 1ms kechikish olamiz, ko'pincha bu muhim emas, lekin nima uchun dasturiy ta'minotni bo'lishi mumkin bo'lganidan yomonroq qilish kerak? Agar biz Thread.Sleep (1) ni olib tashlasak, unda behuda biz bir protsessor yadrosini 100% bo'sh holda yuklaymiz, foydasiz tsiklda aylanadi. TaskCompletionSource-dan foydalanib, siz ushbu kodni osongina qayta yaratishingiz va yuqorida ko'rsatilgan barcha muammolarni hal qilishingiz mumkin:

class LongPollingApi {

    private Dictionary<int, TaskCompletionSource<Msg>> tasks;

    public async Task<Msg> AcceptMessageAsync(int userId, int duration) {

        var cs = new TaskCompletionSource<Msg>();
        tasks[userId] = cs;
        await Task.WhenAny(Task.Delay(duration), cs.Task);
        return cs.Task.IsCompleted ? cs.Task.Result : null;
    }

    public void SendMessage(int userId, Msg m) {

        if (tasks.TryGetValue(userId, out var completionSource))
            completionSource.SetResult(m);
    }
}

Bu kod ishlab chiqarishga tayyor emas, shunchaki demo. Uni real holatlarda ishlatish uchun sizga hech bo'lmaganda xabar hech kim kutmagan vaqtda kelgan vaziyatni hal qilish kerak: bu holda AsseptMessageAsync usuli allaqachon tugallangan vazifani qaytarishi kerak. Agar bu eng keng tarqalgan holat bo'lsa, ValueTask-dan foydalanish haqida o'ylashingiz mumkin.

Xabar so'rovini olganimizda, biz lug'atda TaskCompletionSource yaratamiz va joylashtiramiz va keyin nima bo'lishini kutamiz: belgilangan vaqt oralig'i tugaydi yoki xabar qabul qilinadi.

ValueTask: nima uchun va qanday qilib

Async/await operatorlari, xuddi rentabellikni qaytarish operatori kabi, usuldan holat mashinasini yaratadi va bu yangi ob'ektni yaratishdir, bu deyarli har doim ham muhim emas, lekin kamdan-kam hollarda muammo tug'dirishi mumkin. Bu holat haqiqatan ham tez-tez chaqiriladigan usul bo'lishi mumkin, biz soniyada o'nlab va yuz minglab qo'ng'iroqlar haqida gapiramiz. Agar bunday usul ko'p hollarda barcha kutish usullarini chetlab o'tgan natijani qaytaradigan tarzda yozilgan bo'lsa, u holda .NET buni optimallashtirish uchun vosita - ValueTask tuzilishini taqdim etadi. Buni tushunarli qilish uchun keling, undan foydalanish misolini ko'rib chiqaylik: biz tez-tez boradigan kesh mavjud. Unda ba'zi qiymatlar mavjud va biz ularni shunchaki qaytaramiz, agar bo'lmasa, ularni olish uchun sekin IO ga o'tamiz. Men ikkinchisini asinxron qilishni xohlayman, ya'ni butun usul asinxron bo'lib chiqadi. Shunday qilib, usulni yozishning aniq usuli quyidagicha:

public async Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return val;
    return await RequestById(id);
}

Bir oz optimallashtirish istagi va ushbu kodni tuzishda Roslyn nima yaratishidan ozgina qo'rquv tufayli siz ushbu misolni quyidagicha qayta yozishingiz mumkin:

public Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return Task.FromResult(val);
    return RequestById(id);
}

Haqiqatan ham, bu holatda optimal echim issiq yo'lni optimallashtirish, ya'ni lug'atdan qiymatni keraksiz ajratmasdan va GC-ga yuklamasdan olish bo'ladi, kamdan-kam hollarda ma'lumotlar uchun IO-ga borishimiz kerak bo'ladi. , hamma narsa eski usulda ortiqcha / minus bo'lib qoladi:

public ValueTask<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return new ValueTask<string>(val);
    return new ValueTask<string>(RequestById(id));
}

Keling, ushbu kod qismini batafsil ko'rib chiqaylik: agar keshda qiymat mavjud bo'lsa, biz strukturani yaratamiz, aks holda haqiqiy vazifa mazmunli bo'ladi. Qo'ng'iroq qiluvchi kod bu kod qaysi yo'lda bajarilganiga ahamiyat bermaydi: ValueTask, C# sintaksisi nuqtai nazaridan, bu holda oddiy Task bilan bir xil ishlaydi.

TaskSchedulers: vazifalarni ishga tushirish strategiyalarini boshqarish

Men ko'rib chiqmoqchi bo'lgan keyingi API - bu sinf Vazifalar jadvali va uning hosilalari. Men yuqorida aytib o'tgan edimki, TPL vazifalarni mavzular bo'ylab tarqatish strategiyalarini boshqarish qobiliyatiga ega. Bunday strategiyalar TaskScheduler sinfining avlodlarida aniqlanadi. Sizga kerak bo'lgan deyarli har qanday strategiyani kutubxonada topish mumkin. Parallel kengaytmalar, Microsoft tomonidan ishlab chiqilgan, lekin .NET ning bir qismi emas, lekin Nuget paketi sifatida taqdim etilgan. Keling, ulardan ba'zilarini qisqacha ko'rib chiqaylik:

  • CurrentThreadTaskScheduler — joriy mavzudagi vazifalarni bajaradi
  • LimitedConcurrencyLevelTaskScheduler — konstruktorda qabul qilingan N parametri bilan bir vaqtda bajariladigan vazifalar sonini cheklaydi.
  • OrderedTaskScheduler — LimitedConcurrencyLevelTaskScheduler(1) sifatida aniqlanadi, shuning uchun vazifalar ketma-ket bajariladi.
  • WorkStealingTaskScheduler - asboblar mehnatni o'g'irlash vazifalarni taqsimlashga yondashuv. Aslida, bu alohida ThreadPool. .NET ThreadPool-da barcha ilovalar uchun bitta statik sinf ekanligi muammosini hal qiladi, ya'ni dasturning bir qismida uning ortiqcha yuklanishi yoki noto'g'ri ishlatilishi boshqasida nojo'ya ta'sirlarga olib kelishi mumkin. Bundan tashqari, bunday nuqsonlarning sababini tushunish juda qiyin. Bu. ThreadPool-dan foydalanish tajovuzkor va oldindan aytib bo'lmaydigan bo'lishi mumkin bo'lgan dastur qismlarida alohida WorkStealingTaskSchedulers-dan foydalanish kerak bo'lishi mumkin.
  • QueuedTaskScheduler — ustuvor navbat qoidalariga muvofiq vazifalarni bajarishga imkon beradi
  • ThreadPerTaskScheduler — har bir bajariladigan Task uchun alohida mavzu yaratadi. Bajarilishi oldindan aytib bo'lmaydigan darajada uzoq vaqt talab qiladigan vazifalar uchun foydali bo'lishi mumkin.

Yaxshi tafsilot mavjud maqola Microsoft blogidagi TaskSchedulers haqida.

Vazifalar bilan bog'liq barcha narsalarni qulay disk raskadrovka qilish uchun Visual Studio'da Vazifalar oynasi mavjud. Ushbu oynada siz vazifaning joriy holatini ko'rishingiz va hozirda bajarilayotgan kod qatoriga o'tishingiz mumkin.

.NET: Ko'p oqimli va asinxroniya bilan ishlash uchun asboblar. 1-qism

PLinq va Parallel klassi

Vazifalar va ular haqida aytilgan barcha narsalardan tashqari, .NETda yana ikkita qiziqarli vosita mavjud: PLinq (Linq2Parallel) va Parallel klassi. Birinchisi, barcha Linq operatsiyalarining bir nechta iplarda parallel bajarilishini va'da qiladi. Mavzular sonini WithDegreeOfParallelism kengaytmasi usuli yordamida sozlash mumkin. Afsuski, ko'pincha standart rejimda PLinq sezilarli tezlikni oshirish uchun ma'lumotlar manbangizning ichki qismlari haqida etarli ma'lumotga ega emas, boshqa tomondan, sinab ko'rish narxi juda past: siz shunchaki AsParallel usulini chaqirishingiz kerak. Linq usullari zanjiri va ishlash testlarini o'tkazing. Bundan tashqari, bo'limlar mexanizmidan foydalanib, PLinq-ga ma'lumotlar manbangizning tabiati haqida qo'shimcha ma'lumotlarni uzatishingiz mumkin. Siz ko'proq o'qishingiz mumkin shu yerda и shu yerda.

Parallel statik klassi Foreach to'plamini parallel ravishda takrorlash, For tsiklini bajarish va parallel Invoke-da bir nechta delegatlarni bajarish usullarini taqdim etadi. Joriy ipning bajarilishi hisob-kitoblar tugamaguncha to'xtatiladi. Tarmoqlar sonini ParallelOptions ni oxirgi argument sifatida o'tkazish orqali sozlash mumkin. Variantlar yordamida TaskScheduler va CancellationToken ni ham belgilashingiz mumkin.

topilmalar

Hisobotim materiallari va undan keyingi ishim davomida to‘plagan ma’lumotlarim asosida ushbu maqolani yozishni boshlaganimda, bunchalik ko‘p bo‘lishini kutmagandim. Endi, men ushbu maqolani yozayotgan matn muharriri menga 15-sahifa tugaganini qoralaganida, men oraliq natijalarni umumlashtiraman. Boshqa fokuslar, API'lar, vizual vositalar va tuzoqlar keyingi maqolada ko'rib chiqiladi.

Natijalar:

  • Zamonaviy shaxsiy kompyuterlarning resurslaridan foydalanish uchun iplar, asinxroniya va parallelizm bilan ishlash vositalarini bilishingiz kerak.
  • .NETda bu maqsadlar uchun juda ko'p turli xil vositalar mavjud
  • Ularning barchasi bir vaqtning o'zida paydo bo'lmagan, shuning uchun siz ko'pincha eskisini topishingiz mumkin, ammo eski API-larni ko'p harakat qilmasdan aylantirish usullari mavjud.
  • .NET da iplar bilan ishlash Thread va ThreadPool sinflari bilan ifodalanadi
  • Thread.Abort, Thread.Interrupt va Win32 API TerminateThread usullari xavfli va ulardan foydalanish tavsiya etilmaydi. Buning o'rniga CancellationToken mexanizmidan foydalanish yaxshiroqdir
  • Oqim qimmatli resurs bo'lib, uning ta'minoti cheklangan. Mavzular voqealarni kutish bilan band bo'lgan vaziyatlardan qochish kerak. Buning uchun TaskCompletionSource sinfidan foydalanish qulay
  • Parallelizm va asinxroniya bilan ishlash uchun eng kuchli va ilg'or .NET vositalari Vazifalardir.
  • C# async/await operatorlari bloklanmaydigan kutish kontseptsiyasini amalga oshiradi
  • TaskScheduler-dan olingan sinflardan foydalanib, vazifalarning ish zarralari bo'ylab taqsimlanishini boshqarishingiz mumkin
  • ValueTask strukturasi tezkor yo'llar va xotira trafigini optimallashtirishda foydali bo'lishi mumkin
  • Visual Studio ning Tasks and Threads oynalari ko'p tarmoqli yoki asinxron kodni tuzatish uchun foydali bo'lgan ko'plab ma'lumotlarni taqdim etadi.
  • PLinq - bu ajoyib vosita, lekin u sizning ma'lumotlar manbangiz haqida etarli ma'lumotga ega bo'lmasligi mumkin, ammo uni qismlarga ajratish mexanizmi yordamida tuzatish mumkin.
  • Davomi bor…

Manba: www.habr.com

a Izoh qo'shish