.NET: Zana za kufanya kazi na multithreading na asynchrony. Sehemu 1

Ninachapisha nakala asili juu ya Habr, ambayo tafsiri yake imewekwa katika shirika chapisho la blogi.

Haja ya kufanya kitu asynchronously, bila kusubiri matokeo hapa na sasa, au kugawanya kazi kubwa kati ya vitengo kadhaa vinavyoifanya, ilikuwepo kabla ya ujio wa kompyuta. Kwa ujio wao, hitaji hili likawa dhahiri sana. Sasa, mnamo 2019, ninaandika nakala hii kwenye kompyuta ndogo iliyo na kichakataji cha msingi cha Intel Core 8, ambacho michakato zaidi ya mia moja inaendesha sambamba, na nyuzi zaidi. Karibu, kuna simu ya shabby kidogo, iliyonunuliwa miaka michache iliyopita, ina processor 8-msingi kwenye ubao. Nyenzo za mada zimejaa makala na video ambapo waandishi wao wanapenda simu mahiri mahiri za mwaka huu ambazo zina vichakataji 16-msingi. MS Azure hutoa mashine pepe yenye kichakataji cha msingi 20 na RAM ya TB 128 kwa chini ya $2/saa. Kwa bahati mbaya, haiwezekani kutoa kiwango cha juu na kutumia nguvu hii bila kuwa na uwezo wa kudhibiti mwingiliano wa nyuzi.

Terminology

Mchakato - Kitu cha OS, nafasi ya anwani iliyotengwa, ina nyuzi.
Uzi - kitu cha OS, kitengo kidogo zaidi cha utekelezaji, sehemu ya mchakato, nyuzi hushiriki kumbukumbu na rasilimali zingine kati yao wenyewe ndani ya mchakato.
Multitasking - Mali ya OS, uwezo wa kuendesha michakato kadhaa wakati huo huo
Multi-msingi - mali ya processor, uwezo wa kutumia cores kadhaa kwa usindikaji wa data
Usindikaji mwingi - mali ya kompyuta, uwezo wa kufanya kazi wakati huo huo na wasindikaji kadhaa kimwili
Usomaji mwingi - mali ya mchakato, uwezo wa kusambaza usindikaji wa data kati ya nyuzi kadhaa.
Usambamba - kufanya vitendo kadhaa kwa wakati mmoja kwa kila kitengo cha wakati
Asynchrony - Utekelezaji wa operesheni bila kungoja kukamilika kwa usindikaji huu; matokeo ya utekelezaji yanaweza kusindika baadaye.

Sitiari

Sio fasili zote ni nzuri na zingine zinahitaji maelezo ya ziada, kwa hivyo nitaongeza sitiari kuhusu kupika kifungua kinywa kwenye istilahi iliyoletwa rasmi. Kupika kifungua kinywa katika sitiari hii ni mchakato.

Wakati wa kuandaa kifungua kinywa asubuhi mimi (CPU) Ninakuja jikoni (Kompyuta) Nina mikono 2 (vipande) Kuna vifaa kadhaa jikoni (IO): tanuri, kettle, kibaniko, jokofu. Ninawasha gesi, weka sufuria juu yake na kumwaga mafuta ndani yake bila kungoja iwake moto (asynchronously, isiyo ya Kuzuia-IO-Subiri), ninachukua mayai kutoka kwenye jokofu na kuwavunja kwenye sahani, kisha kuwapiga kwa mkono mmoja (Mfululizo #1), na pili (Mfululizo #2) kushikilia sahani (Nyenzo ya Pamoja). Sasa ningependa kuwasha kettle, lakini sina mikono ya kutosha (Uzi Njaa) Wakati huu, sufuria ya kukaanga huwaka (Kusindika matokeo) ambayo mimi humimina kile nilichopiga. Ninafikia kettle na kuiwasha na kwa ujinga kutazama maji yakichemka ndani yake (Kuzuia-IO-Subiri), ingawa wakati huu angeweza kuosha sahani ambapo alipiga omelet.

Nilipika omelet kwa kutumia mikono 2 tu, na sina zaidi, lakini wakati huo huo, wakati wa kupiga omeleti, shughuli 3 zilifanyika mara moja: kupiga omeleti, kushikilia sahani, inapokanzwa sufuria ya kukaanga. CPU ndio sehemu ya kasi zaidi ya kompyuta, IO ndio ambayo mara nyingi kila kitu hupungua, kwa hivyo mara nyingi suluhisho bora ni kuchukua CPU na kitu wakati unapokea data kutoka kwa IO.

Kuendeleza sitiari:

  • Ikiwa katika mchakato wa kuandaa omelet, ningejaribu pia kubadilisha nguo, hii itakuwa mfano wa multitasking. Nuance muhimu: kompyuta ni bora zaidi kwa hili kuliko watu.
  • Jikoni na wapishi kadhaa, kwa mfano katika mgahawa - kompyuta ya msingi mbalimbali.
  • Migahawa mingi katika mahakama ya chakula katika kituo cha ununuzi - kituo cha data

Zana za NET

.NET ni mzuri katika kufanya kazi na nyuzi, kama ilivyo kwa vitu vingine vingi. Kwa kila toleo jipya, huleta zana zaidi na zaidi za kufanya kazi nazo, tabaka mpya za uondoaji juu ya nyuzi za OS. Wakati wa kufanya kazi na ujenzi wa vifupisho, watengenezaji wa mfumo hutumia mbinu inayoacha fursa, wakati wa kutumia uondoaji wa hali ya juu, kwenda chini ngazi moja au zaidi. Mara nyingi hii sio lazima, kwa kweli inafungua mlango wa kujipiga risasi mguuni na bunduki, lakini wakati mwingine, katika hali nadra, inaweza kuwa njia pekee ya kutatua shida ambayo haijatatuliwa kwa kiwango cha sasa cha kujiondoa. .

Kwa zana, ninamaanisha miingiliano ya programu ya programu (API) iliyotolewa na mfumo na vifurushi vya watu wengine, pamoja na suluhisho zima la programu ambazo hurahisisha utaftaji wa shida zozote zinazohusiana na nambari zenye nyuzi nyingi.

Kuanzisha thread

Darasa la Thread ndilo darasa la msingi zaidi katika .NET la kufanya kazi na nyuzi. Mjenzi anakubali mmoja wa wajumbe wawili:

  • ThreadStart - Hakuna vigezo
  • ParametrizedThreadStart - na parameter moja ya kitu cha aina.

Mjumbe atatekelezwa katika uzi mpya ulioundwa baada ya kuita mbinu ya Anza. Ikiwa mjumbe wa aina ya ParametrizedThreadStart alipitishwa kwa mjenzi, basi kitu lazima kipitishwe kwa mbinu ya Anza. Utaratibu huu unahitajika ili kuhamisha taarifa zozote za ndani hadi kwenye mkondo. Ni muhimu kuzingatia kwamba kuunda thread ni operesheni ya gharama kubwa, na thread yenyewe ni kitu kizito, angalau kwa sababu inatenga 1MB ya kumbukumbu kwenye stack na inahitaji mwingiliano na OS API.

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

Darasa la ThreadPool linawakilisha dhana ya bwawa. Katika .NET, dimbwi la uzi ni sehemu ya uhandisi, na wasanidi programu katika Microsoft wameweka juhudi nyingi katika kuhakikisha kuwa inafanya kazi vyema katika aina mbalimbali za matukio.

Dhana ya jumla:

Kuanzia wakati programu inapoanza, huunda nyuzi kadhaa kwenye hifadhi nyuma na hutoa uwezo wa kuzichukua kwa matumizi. Ikiwa nyuzi zinatumiwa mara kwa mara na kwa idadi kubwa, bwawa hupanuka ili kukidhi mahitaji ya mpigaji simu. Wakati hakuna nyuzi za bure kwenye bwawa kwa wakati unaofaa, itasubiri moja ya nyuzi kurudi, au kuunda mpya. Inafuata kwamba mkusanyiko wa mazungumzo ni mzuri kwa vitendo vingine vya muda mfupi na haufai vyema kwa shughuli zinazoendeshwa kama huduma wakati wote wa utendakazi wa programu.

Ili kutumia uzi kutoka kwenye bwawa, kuna mbinu ya QueueUserWorkItem inayokubali mjumbe wa aina ya WaitCallback, ambayo ina sahihi sawa na ParametrizedThreadStart, na kigezo kinachopitishwa kwake hufanya kazi sawa.

ThreadPool.QueueUserWorkItem(...);

Mbinu isiyojulikana sana ya kujiandikisha ya RegisterWaitForSingleObject inatumiwa kupanga shughuli za IO zisizozuia. Mjumbe aliyepitishwa kwa njia hii ataitwa wakati WaitHandle iliyopitishwa kwa njia "Imetolewa".

ThreadPool.RegisterWaitForSingleObject(...)

.NET ina kipima muda cha nyuzi na inatofautiana na vipima muda vya WinForms/WPF kwa kuwa kidhibiti chake kitaitwa kwenye uzi uliochukuliwa kutoka kwenye bwawa.

System.Threading.Timer

Pia kuna njia ya kigeni ya kutuma mjumbe kwa ajili ya utekelezaji kwenye thread kutoka kwenye bwawa - njia ya BeginInvoke.

DelegateInstance.BeginInvoke

Ningependa kukaa kwa ufupi juu ya kazi ambayo njia nyingi hapo juu zinaweza kuitwa - CreateThread kutoka Kernel32.dll Win32 API. Kuna njia, shukrani kwa utaratibu wa mbinu za nje, kuita kazi hii. Nimeona simu kama hiyo mara moja tu katika mfano mbaya wa nambari ya urithi, na motisha ya mwandishi ambaye alifanya hivi haswa bado ni siri kwangu.

Kernel32.dll CreateThread

Kuangalia na Kutatua Nyuzi

Mazungumzo uliyounda, vipengele vyote vya wahusika wengine, na bwawa la NET vinaweza kutazamwa kwenye dirisha la Threads la Visual Studio. Dirisha hili litaonyesha tu habari ya mazungumzo wakati programu iko chini ya utatuzi na iko katika hali ya Kuvunja. Hapa unaweza kutazama kwa urahisi majina ya rafu na vipaumbele vya kila uzi, na ubadilishe utatuzi hadi uzi maalum. Kwa kutumia kipengele cha Kipaumbele cha darasa la Thread, unaweza kuweka kipaumbele cha thread, ambayo OC na CLR watatambua kama pendekezo wakati wa kugawanya muda wa kichakataji kati ya nyuzi.

.NET: Zana za kufanya kazi na multithreading na asynchrony. Sehemu 1

Maktaba Sambamba ya Kazi

Maktaba ya Task Parallel (TPL) ilianzishwa katika .NET 4.0. Sasa ni kiwango na chombo kuu cha kufanya kazi na asynchrony. Nambari yoyote inayotumia mbinu ya zamani inachukuliwa kuwa urithi. Sehemu ya msingi ya TPL ni darasa la Task kutoka kwa System.Threading.Tasks namespace. Jukumu ni kifupi juu ya uzi. Kwa toleo jipya la lugha ya C#, tulipata njia maridadi ya kufanya kazi na Tasks - async/ait operators. Dhana hizi zilifanya iwezekane kuandika msimbo wa asynchronous kana kwamba ni rahisi na ya kusawazisha, hii ilifanya iwezekane hata kwa watu wenye uelewa mdogo wa utendakazi wa ndani wa nyuzi kuandika programu zinazotumia, programu ambazo hazifungii wakati wa kufanya shughuli ndefu. Kutumia async/ait ni mada ya nakala moja au hata kadhaa, lakini nitajaribu kupata kiini chake katika sentensi chache:

  • async ni kirekebishaji cha njia ya kurejesha Task au batili
  • na kusubiri ni opereta asiyezuia Task kusubiri.

Kwa mara nyingine tena: operator wa kusubiri, kwa hali ya jumla (kuna tofauti), atatoa thread ya sasa ya utekelezaji zaidi, na wakati Task itamaliza utekelezaji wake, na thread (kwa kweli, itakuwa sahihi zaidi kusema muktadha. , lakini zaidi juu ya hilo baadaye) itaendelea kutekeleza njia zaidi. Ndani ya NET, utaratibu huu unatekelezwa kwa njia sawa na kurudi kwa mavuno, wakati njia iliyoandikwa inageuka kuwa darasa zima, ambayo ni mashine ya serikali na inaweza kutekelezwa kwa vipande tofauti kulingana na majimbo haya. Mtu yeyote anayevutiwa anaweza kuandika msimbo wowote rahisi kwa kutumia asynс/await, kukusanya na kutazama mkusanyiko kwa kutumia JetBrains dotPeek na Msimbo Unaozalishwa wa Mkusanyaji umewashwa.

Wacha tuangalie chaguzi za kuzindua na kutumia Task. Katika mfano wa nambari hapa chini, tunaunda kazi mpya ambayo haifanyi chochote muhimu (Uzi.Kulala(10000)), lakini katika maisha halisi hii inapaswa kuwa kazi ngumu ya kutumia CPU.

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
}

Kazi imeundwa na idadi ya chaguzi:

  • LongRunning ni kidokezo kwamba kazi haitakamilika haraka, ambayo inamaanisha inaweza kuwa na thamani ya kuzingatia kutochukua thread kutoka kwenye bwawa, lakini kuunda moja tofauti kwa Kazi hii ili si kuwadhuru wengine.
  • IliyoambatishwaKwaParent - Kazi zinaweza kupangwa katika daraja. Ikiwa chaguo hili lilitumiwa, basi Kazi inaweza kuwa katika hali ambayo yenyewe imekamilisha na inasubiri utekelezaji wa watoto wake.
  • PreferFairness - inamaanisha kuwa itakuwa bora kutekeleza Majukumu yaliyotumwa kutekelezwa mapema kabla ya yale yaliyotumwa baadaye. Lakini hii ni pendekezo tu na matokeo hayahakikishiwa.

Kigezo cha pili kilichopitishwa kwa njia ni CancellationToken. Ili kushughulikia kwa usahihi kughairiwa kwa operesheni baada ya kuanza, msimbo unaotekelezwa lazima ujazwe na ukaguzi wa hali ya CancellationToken. Ikiwa hakuna ukaguzi, basi njia ya Kughairi inayoitwa kwenye kitu cha CancellationTokenSource itaweza kusimamisha utekelezaji wa Jukumu kabla tu ya kuanza.

Kigezo cha mwisho ni kitu cha mpangilio wa aina ya TaskScheduler. Darasa hili na vizazi vyake vimeundwa ili kudhibiti mikakati ya kusambaza Majukumu kwenye minyororo; kwa chaguo-msingi, Jukumu litatekelezwa kwa mazungumzo nasibu kutoka kwenye dimbwi.

Opereta ya kusubiri inatumika kwa Kazi iliyoundwa, ambayo inamaanisha msimbo ulioandikwa baada yake, ikiwa kuna moja, utatekelezwa katika muktadha sawa (mara nyingi hii ina maana kwenye thread sawa) kama msimbo kabla ya kusubiri.

Njia hiyo imetiwa alama kuwa ni batili ya async, ambayo inamaanisha inaweza kutumia opereta anayesubiri, lakini msimbo wa kupiga simu hautaweza kusubiri utekelezaji. Ikiwa kipengele hicho ni muhimu, basi njia lazima irudi Kazi. Mbinu zilizo na alama ya utupu wa async ni za kawaida sana: kama sheria, hizi ni vidhibiti vya hafla au njia zingine zinazofanya kazi kwenye moto na kusahau kanuni. Ikiwa unahitaji si tu kutoa fursa ya kusubiri hadi mwisho wa utekelezaji, lakini pia kurudi matokeo, basi unahitaji kutumia Task.

Kwenye Kazi ambayo njia ya StartNew ilirudi, na vile vile kwa nyingine yoyote, unaweza kupiga njia ya ConfigureAwait na parameta ya uwongo, kisha utekelezaji baada ya kungojea utaendelea sio kwa muktadha uliokamatwa, lakini kwa kiholela. Hii inapaswa kufanywa kila wakati wakati muktadha wa utekelezaji sio muhimu kwa nambari baada ya kungojea. Hili pia ni pendekezo kutoka kwa MS wakati wa kuandika msimbo ambao utawasilishwa kwa vifurushi kwenye maktaba.

Hebu tuzingatie zaidi jinsi unavyoweza kusubiri kukamilika kwa Kazi. Ifuatayo ni mfano wa msimbo, na maoni juu ya wakati matarajio yanafanywa vizuri kwa masharti na inapofanywa vibaya kwa masharti.

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
}

Katika mfano wa kwanza, tunangojea Kazi ikamilike bila kuzuia uzi wa kupiga simu; tutarudi kushughulikia matokeo tu wakati tayari iko; hadi wakati huo, uzi wa kupiga simu unaachwa kwa vifaa vyake.

Katika chaguo la pili, tunazuia thread ya wito mpaka matokeo ya njia yamehesabiwa. Hii ni mbaya sio tu kwa sababu tumechukua uzi, rasilimali muhimu ya programu, na uvivu rahisi, lakini pia kwa sababu ikiwa nambari ya njia tunayoita inangojea, na muktadha wa maingiliano unahitaji kurudi kwenye uzi wa kupiga simu baada ya. subiri, basi tutapata msuguano : Kamba ya kupiga simu inasubiri wakati matokeo ya njia ya asynchronous yamehesabiwa, njia ya asynchronous inajaribu bure kuendelea na utekelezaji wake katika thread ya kupiga simu.

Ubaya mwingine wa njia hii ni kushughulikia makosa. Ukweli ni kwamba makosa katika msimbo wa asynchronous wakati wa kutumia async/await ni rahisi sana kushughulikia - wanafanya sawa na kwamba msimbo ulikuwa wa kusawazisha. Ingawa tukiweka mapepo ya kusubiri kwa usawaziko kwa Task, ubaguzi asili hubadilika kuwa AggregateException, i.e. Ili kushughulikia ubaguzi, itabidi uchunguze aina ya InnerException na uandike ikiwa mnyororo mwenyewe ndani ya kizuizi kimoja cha kukamata au utumie samaki wakati wa kuunda, badala ya safu ya vizuizi vya kukamata ambayo inajulikana zaidi katika ulimwengu wa C#.

Mifano ya tatu na ya mwisho pia imewekwa alama mbaya kwa sababu sawa na ina shida zote sawa.

Njia za WhenAny na WhenAll ni rahisi sana kungojea kikundi cha Majukumu; hufunga kikundi cha Majukumu kuwa moja, ambayo itawaka wakati Jukumu kutoka kwa kikundi linapoanzishwa, au wakati zote zimekamilisha utekelezaji wao.

Kusimamisha nyuzi

Kwa sababu mbalimbali, inaweza kuwa muhimu kuacha mtiririko baada ya kuanza. Kuna idadi ya njia za kufanya hivyo. Darasa la Thread lina njia mbili zinazoitwa ipasavyo: Toa mimba ΠΈ Kukatiza. Ya kwanza haipendekezi sana kwa matumizi, kwa sababu baada ya kuiita wakati wowote wa nasibu, wakati wa usindikaji wa maagizo yoyote, ubaguzi utatupwa ThreadAbortedException. Hutarajii ubaguzi kama huo kutupwa wakati wa kuongeza utofauti wowote kamili, sivyo? Na wakati wa kutumia njia hii, hii ni hali halisi sana. Ikiwa unahitaji kuzuia CLR kutoa ubaguzi kama huo katika sehemu fulani ya msimbo, unaweza kuifunika kwa simu. Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Nambari yoyote iliyoandikwa kwenye kizuizi hatimaye imefungwa kwa simu kama hizo. Kwa sababu hii, katika kina cha msimbo wa mfumo unaweza kupata vitalu kwa kujaribu tupu, lakini sio tupu hatimaye. Microsoft hukatisha tamaa njia hii kiasi kwamba hawakuijumuisha kwenye msingi wa .net.

Njia ya Kukatiza inafanya kazi kwa kutabirika zaidi. Inaweza kukatiza uzi kwa ubaguzi ThreadInterruptedException tu wakati huo wakati thread iko katika hali ya kusubiri. Inaingia katika hali hii inaponing'inia inaposubiri WaitHandle, kufunga, au baada ya kupiga simu Thread.Sleep.

Chaguzi zote mbili zilizoelezwa hapo juu ni mbaya kwa sababu ya kutotabirika kwao. Suluhisho ni kutumia muundo CancellationToken na darasa CancellationTokenChanzo. Jambo ni hili: mfano wa darasa la CancellationTokenSource huundwa na ni yule tu anayemiliki ndiye anayeweza kusimamisha operesheni kwa kupiga njia. kufuta. Ni Ishara ya Kughairi pekee ndiyo inapitishwa kwa operesheni yenyewe. Wamiliki wa CancellationToken hawawezi kughairi utendakazi wenyewe, lakini wanaweza tu kuangalia ikiwa operesheni imeghairiwa. Kuna mali ya Boolean kwa hii Imeomba Kughairiwa na mbinu ThrowIfCancelOmbi. Mwisho utatupa ubaguzi TaskCancelledException ikiwa njia ya Kughairi iliitwa kwa mfano wa CancellationToken kubadilishwa. Na hii ndio njia ninayopendekeza kutumia. Huu ni uboreshaji juu ya chaguo za awali kwa kupata udhibiti kamili juu ya wakati ambapo operesheni ya ubaguzi inaweza kusitishwa.

Chaguo la kikatili zaidi la kusimamisha uzi ni kupiga kazi ya Win32 API TerminateThread. Tabia ya CLR baada ya kupiga simu hii inaweza kuwa isiyotabirika. Kwenye MSDN ifuatayo imeandikwa juu ya kazi hii: "TerminateThread ni kazi hatari ambayo inapaswa kutumika tu katika hali mbaya zaidi. "

Kubadilisha API ya urithi hadi Task Based kwa kutumia mbinu ya FromAsync

Iwapo ulikuwa na bahati ya kufanya kazi kwenye mradi ulioanzishwa baada ya Majukumu kuanzishwa na kukoma kusababisha hofu ya utulivu kwa watengenezaji wengi, basi hautalazimika kushughulika na API nyingi za zamani, za wahusika wengine na zile za timu yako. ametesa huko nyuma. Kwa bahati nzuri, timu ya .NET Framework ilitutunza, ingawa labda lengo lilikuwa kujitunza wenyewe. Iwe hivyo, .NET ina zana kadhaa za kubadilisha bila maumivu msimbo ulioandikwa katika mbinu za zamani za upangaji zisizolingana hadi mpya. Mojawapo ni njia ya FromAsync ya TaskFactory. Katika mfano wa nambari hapa chini, ninafunga njia za zamani za async za darasa la WebRequest kwenye Task kwa kutumia njia hii.

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

Huu ni mfano tu na hakuna uwezekano wa kufanya hivi na aina zilizojengewa ndani, lakini mradi wowote wa zamani umejaa mbinu za BeginDoSomething ambazo hurejesha njia za IAsyncResult na EndDoSomething zinazoipokea.

Badilisha API ya urithi kuwa Task Based kwa kutumia TaskCompletionSource darasa

Chombo kingine muhimu cha kuzingatia ni darasa TaskCompletionChanzo. Kwa upande wa kazi, madhumuni na kanuni ya utendakazi, inaweza kukumbusha kwa kiasi fulani njia ya RegisterWaitForSingleObject ya darasa la ThreadPool, ambayo niliandika juu yake hapo juu. Kwa kutumia darasa hili, unaweza kufunga kwa urahisi na kwa urahisi API za zamani za Asynchronous kwenye Majukumu.

Utasema kwamba tayari nimezungumza juu ya njia ya FromAsync ya darasa la TaskFactory iliyokusudiwa kwa madhumuni haya. Hapa itabidi tukumbuke historia nzima ya ukuzaji wa miundo ya asynchronous katika .net ambayo Microsoft imetoa kwa kipindi cha miaka 15 iliyopita: kabla ya Task-Based Asynchronous Pattern (TAP), kulikuwa na Asynchronous Programming Pattern (APP), ambayo ilihusu mbinu KuanzaFanya Kitu kinarudi IAsyncResult na mbinu mwishoDoSomething ambayo inaikubali na kwa urithi wa miaka hii njia ya FromAsync ni sawa tu, lakini baada ya muda, ilibadilishwa na Mchoro wa Tukio wa Asynchronous (EAP), ambayo ilidhani kuwa tukio lingeinuliwa wakati operesheni ya asynchronous itakamilika.

TaskCompletionSource ni kamili kwa kufunga Majukumu na API za urithi zilizoundwa karibu na muundo wa tukio. Kiini cha kazi yake ni kama ifuatavyo: kitu cha darasa hili kina mali ya umma ya aina ya Task, hali ambayo inaweza kudhibitiwa kupitia SetResult, SetException, nk mbinu za darasa la TaskCompletionSource. Katika maeneo ambapo opereta anayesubiri alitumiwa kwa Jukumu hili, itatekelezwa au itashindwa isipokuwa kulingana na njia iliyotumika kwenye Chanzo cha TaskCompletion. Ikiwa bado haijulikani wazi, hebu tuangalie mfano huu wa msimbo, ambapo API ya zamani ya EAP imefungwa kwenye Task kwa kutumia TaskCompletionSource: tukio linapowaka, Kazi itahamishiwa kwenye hali Iliyokamilishwa, na njia iliyotumia opereta anayesubiri. kwa Jukumu hili itaendelea utekelezaji wake baada ya kupokea kitu kusababisha.

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

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

    result completionSource.Task;
}

Vidokezo na Mbinu za TaskCompletionSource

Kufunga API za zamani sio yote ambayo yanaweza kufanywa kwa kutumia TaskCompletionSource. Kutumia darasa hili kunafungua uwezekano wa kuvutia wa kubuni API mbalimbali kwenye Majukumu ambayo hayajashughulikiwa na nyuzi. Na mkondo, kama tunavyokumbuka, ni rasilimali ya gharama kubwa na idadi yao ni mdogo (haswa na kiasi cha RAM). Kizuizi hiki kinaweza kupatikana kwa urahisi kwa kukuza, kwa mfano, programu ya wavuti iliyopakiwa na mantiki ngumu ya biashara. Wacha tuzingatie uwezekano ambao ninazungumza wakati wa kutekeleza hila kama Upigaji kura wa Muda Mrefu.

Kwa kifupi, kiini cha hila ni hii: unahitaji kupokea taarifa kutoka kwa API kuhusu matukio fulani yanayotokea upande wake, wakati API, kwa sababu fulani, haiwezi kuripoti tukio hilo, lakini inaweza tu kurudisha hali. Mfano wa hizi ni API zote zilizojengwa juu ya HTTP kabla ya nyakati za WebSocket au wakati ambapo haikuwezekana kwa sababu fulani kutumia teknolojia hii. Mteja anaweza kuuliza seva ya HTTP. Seva ya HTTP haiwezi yenyewe kuanzisha mawasiliano na mteja. Suluhisho rahisi ni kupigia kura seva kwa kutumia kipima muda, lakini hii huleta mzigo wa ziada kwenye seva na kucheleweshwa zaidi kwa wastani wa Muda wa Muda / 2. Ili kukabiliana na hili, hila inayoitwa Upigaji kura kwa muda mrefu ilivumbuliwa, ambayo inahusisha kuchelewesha majibu kutoka. seva hadi Muda wa Kuisha uishe au tukio litatokea. Ikiwa tukio limetokea, basi linasindika, ikiwa sio, basi ombi linatumwa tena.

while(!eventOccures && !timeoutExceeded)  {

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

Lakini suluhisho kama hilo litaonekana kuwa mbaya mara tu idadi ya wateja wanaongojea hafla hiyo inavyoongezeka, kwa sababu ... Kila mteja kama huyo anashikilia uzi mzima akingojea tukio. Ndiyo, na tunapata ucheleweshaji wa ziada wa 1ms tukio linapoanzishwa, mara nyingi hii si muhimu, lakini kwa nini kufanya programu kuwa mbaya zaidi kuliko inaweza kuwa? Ikiwa tunaondoa Thread.Sleep (1), basi bure tutapakia msingi mmoja wa processor 100% bila kazi, ikizunguka katika mzunguko usio na maana. Kwa kutumia TaskCompletionSource unaweza kutengeneza tena nambari hii kwa urahisi na kutatua matatizo yote yaliyoainishwa hapo juu:

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);
    }
}

Msimbo huu hauko tayari kwa uzalishaji, lakini ni onyesho tu. Ili kuitumia katika hali halisi, unahitaji pia, kwa kiwango cha chini, kushughulikia hali wakati ujumbe unapofika wakati hakuna mtu anayeutarajia: katika kesi hii, njia ya AsseptMessageAsync inapaswa kurudisha Kazi iliyokamilishwa tayari. Ikiwa hii ndiyo kesi ya kawaida, basi unaweza kufikiri juu ya kutumia ValueTask.

Tunapopokea ombi la ujumbe, tunaunda na kuweka TaskCompletionSource katika kamusi, na kisha kusubiri kitakachotokea kwanza: muda uliobainishwa unaisha au ujumbe unapokelewa.

ValueTask: kwa nini na jinsi gani

Waendeshaji wa async/wait, kama opereta wa kurudi kwa mavuno, hutoa mashine ya hali kutoka kwa mbinu, na huu ni uundaji wa kitu kipya, ambacho karibu kila wakati sio muhimu, lakini katika hali nadra inaweza kuunda shida. Kesi hii inaweza kuwa njia ambayo inaitwa mara nyingi, tunazungumza juu ya makumi na mamia ya maelfu ya simu kwa sekunde. Ikiwa njia hiyo imeandikwa kwa njia ambayo katika hali nyingi inarudi matokeo kwa kupita njia zote za kusubiri, basi NET hutoa chombo cha kuboresha hii - muundo wa ValueTask. Ili kuifanya wazi, hebu tuangalie mfano wa matumizi yake: kuna cache ambayo tunakwenda mara nyingi sana. Kuna maadili kadhaa ndani yake na kisha tunayarudisha tu; ikiwa sivyo, basi tunaenda kwa IO polepole ili kuzipata. Ninataka kufanya haya ya mwisho kwa usawa, ambayo inamaanisha kuwa njia nzima inageuka kuwa ya asynchronous. Kwa hivyo, njia dhahiri ya kuandika njia ni kama ifuatavyo.

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

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

Kwa sababu ya hamu ya kuongeza kidogo, na hofu kidogo ya kile Roslyn atatoa wakati wa kuunda nambari hii, unaweza kuandika tena mfano huu kama ifuatavyo.

public Task<string> GetById(int id) {

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

Hakika, suluhisho bora katika kesi hii itakuwa kuongeza njia ya moto, ambayo ni, kupata thamani kutoka kwa kamusi bila mgao wowote usio wa lazima na kupakia GC, wakati katika hali hizo adimu wakati bado tunahitaji kwenda kwa IO kwa data. , kila kitu kitabaki kuwa plus /minus njia ya zamani:

public ValueTask<string> GetById(int id) {

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

Hebu tuchunguze kwa undani kipande hiki cha msimbo: ikiwa kuna thamani katika cache, tunaunda muundo, vinginevyo kazi halisi itafungwa kwa maana. Msimbo wa kupiga simu haujali ni njia gani nambari hii ilitekelezwa katika: ValueTask, kutoka kwa mtazamo wa sintaksia ya C #, itafanya kazi sawa na Kazi ya kawaida katika kesi hii.

TaskSchedulers: kusimamia mikakati ya uzinduzi wa kazi

API inayofuata ambayo ningependa kuzingatia ni darasa TaskScheduler na derivatives zake. Nilishataja hapo juu kuwa TPL ina uwezo wa kusimamia mikakati ya kusambaza Tasks kwenye nyuzi. Mikakati kama hii imefafanuliwa katika vizazi vya darasa la TaskScheduler. Takriban mkakati wowote unaoweza kuhitaji unaweza kupatikana kwenye maktaba. ParallelExtensionsExtras, iliyotengenezwa na Microsoft, lakini si sehemu ya .NET, lakini imetolewa kama kifurushi cha Nuget. Wacha tuangalie kwa ufupi baadhi yao:

  • CurrentThreadTaskScheduler - hutekeleza Majukumu kwenye uzi wa sasa
  • LimitedConcurrencyLevelTaskScheduler - inapunguza idadi ya Kazi zinazotekelezwa wakati huo huo na parameta N, ambayo inakubaliwa katika mjenzi.
  • OrderedTaskScheduler β€” inafafanuliwa kama LimitedConcurrencyLevelTaskScheduler(1), kwa hivyo majukumu yatatekelezwa kwa kufuatana.
  • WorkStealingTaskScheduler - zana kazi-kuiba mbinu ya usambazaji wa kazi. Kimsingi ni ThreadPool tofauti. Tatua tatizo ambalo katika NET ThreadPool ni darasa la tuli, moja kwa programu zote, ambayo ina maana kwamba upakiaji wake mwingi au matumizi yasiyo sahihi katika sehemu moja ya programu inaweza kusababisha madhara katika nyingine. Kwa kuongezea, ni ngumu sana kuelewa sababu za kasoro kama hizo. Hiyo. Huenda kukawa na haja ya kutumia WorkStealingTaskSchedulers tofauti katika sehemu za programu ambapo matumizi ya ThreadPool yanaweza kuwa ya fujo na yasiyotabirika.
  • FoleniTaskScheduler - hukuruhusu kufanya kazi kulingana na sheria za foleni za kipaumbele
  • ThreadPerTaskScheduler - huunda uzi tofauti kwa kila Kazi ambayo inatekelezwa juu yake. Inaweza kuwa muhimu kwa kazi zinazochukua muda mrefu bila kutabirika kukamilika.

Kuna maelezo mazuri makala kuhusu TaskSchedulers kwenye blogu ya Microsoft.

Kwa utatuzi rahisi wa kila kitu kinachohusiana na Majukumu, Visual Studio ina dirisha la Majukumu. Katika dirisha hili unaweza kuona hali ya sasa ya kazi na kuruka kwenye mstari wa sasa wa utekelezaji wa msimbo.

.NET: Zana za kufanya kazi na multithreading na asynchrony. Sehemu 1

PLinq na darasa la Sambamba

Mbali na Kazi na kila kitu kilichosemwa juu yao, kuna zana mbili za kuvutia zaidi katika NET: PLinq (Linq2Parallel) na darasa la Sambamba. Ya kwanza inaahidi utekelezaji sambamba wa shughuli zote za Linq kwenye nyuzi nyingi. Idadi ya nyuzi zinaweza kusanidiwa kwa kutumia mbinu ya kiendelezi ya WithDegreeOfParallelism. Kwa bahati mbaya, mara nyingi PLinq katika hali yake ya chaguo-msingi haina taarifa za kutosha kuhusu mambo ya ndani ya chanzo chako cha data ili kutoa faida kubwa ya kasi, kwa upande mwingine, gharama ya kujaribu ni ya chini sana: unahitaji tu kupiga simu njia ya AsParallel kabla. mlolongo wa njia za Linq na endesha majaribio ya utendaji. Zaidi ya hayo, inawezekana kupitisha maelezo ya ziada kwa PLinq kuhusu asili ya chanzo chako cha data kwa kutumia utaratibu wa Vigawanyo. Unaweza kusoma zaidi hapa ΠΈ hapa.

Darasa tuli la Sambamba hutoa mbinu za kurudia kupitia mkusanyo wa Foreach sambamba, kutekeleza kwa kitanzi, na kutekeleza wajumbe wengi katika Omba sambamba. Utekelezaji wa thread ya sasa utasimamishwa hadi mahesabu yamekamilika. Idadi ya nyuzi zinaweza kusanidiwa kwa kupitisha ParallelOptions kama hoja ya mwisho. Unaweza pia kubainisha TaskScheduler na CancellationToken kwa kutumia chaguo.

Matokeo

Nilipoanza kuandika makala hii kulingana na nyenzo za ripoti yangu na taarifa nilizokusanya wakati wa kazi yangu baada yake, sikutarajia kwamba kungekuwa na mengi sana. Sasa, wakati mhariri wa maandishi ambamo ninaandika nakala hii ananiambia kwa dharau kwamba ukurasa wa 15 umeenda, nitafanya muhtasari wa matokeo ya muda mfupi. Ujanja mwingine, API, zana za kuona na mitego itafunikwa katika makala inayofuata.

Hitimisho:

  • Unahitaji kujua zana za kufanya kazi na nyuzi, asynchrony na parallelism ili kutumia rasilimali za Kompyuta za kisasa.
  • NET ina zana nyingi tofauti kwa madhumuni haya
  • Sio wote walionekana mara moja, hivyo unaweza kupata mara nyingi urithi, hata hivyo, kuna njia za kubadilisha API za zamani bila jitihada nyingi.
  • Kufanya kazi na nyuzi katika .NET inawakilishwa na madarasa ya Thread na ThreadPool
  • Njia za Thread.Abort, Thread.Interrupt, na Win32 API TerminateThread ni hatari na hazipendekezwi kwa matumizi. Badala yake, ni bora kutumia utaratibu wa CancellationToken
  • Mtiririko ni rasilimali muhimu na usambazaji wake ni mdogo. Hali ambazo nyuzi ziko busy kusubiri matukio zinapaswa kuepukwa. Kwa hili ni rahisi kutumia darasa la TaskCompletionSource
  • Zana zenye nguvu zaidi na za hali ya juu za NET za kufanya kazi kwa usawa na usawazishaji ni Majukumu.
  • Waendeshaji wa c# async/wait hutekeleza wazo la kungoja bila kuzuia
  • Unaweza kudhibiti usambazaji wa Majukumu kwenye minyororo kwa kutumia madarasa yanayotokana na TaskScheduler
  • Muundo wa ValueTask unaweza kuwa muhimu katika kuboresha njia-moto na trafiki ya kumbukumbu
  • Dirisha la Kazi na nyuzi za Visual Studio hutoa habari nyingi muhimu kwa kurekebisha msimbo wa nyuzi nyingi au asynchronous.
  • PLinq ni zana nzuri, lakini inaweza kuwa haina maelezo ya kutosha kuhusu chanzo chako cha data, lakini hii inaweza kusasishwa kwa kutumia utaratibu wa kugawanya.
  • Kuendelea ...

Chanzo: mapenzi.com

Kuongeza maoni