.NET: Offer ar gyfer gweithio gyda multithreading a asynchrony. Rhan 1

Rwy'n cyhoeddi'r erthygl wreiddiol ar Habr, y mae ei chyfieithiad wedi'i bostio yn y corfforaethol post blog.

Roedd yr angen i wneud rhywbeth yn asyncronig, heb aros am y canlyniad yn y fan a'r lle, neu i rannu gwaith mawr rhwng sawl uned yn ei berfformio, yn bodoli cyn dyfodiad cyfrifiaduron. Gyda'u dyfodiad, daeth yr angen hwn yn ddiriaethol iawn. Nawr, yn 2019, rwy'n teipio'r erthygl hon ar liniadur gyda phrosesydd Intel Core 8-craidd, y mae mwy na chant o brosesau'n rhedeg yn gyfochrog arno, a hyd yn oed mwy o edafedd. Gerllaw, mae ffôn ychydig yn ddi-raen, a brynwyd ychydig flynyddoedd yn ôl, mae ganddo brosesydd 8-craidd ar ei fwrdd. Mae adnoddau thematig yn llawn erthyglau a fideos lle mae eu hawduron yn edmygu ffonau smart blaenllaw eleni sy'n cynnwys proseswyr 16-craidd. Mae MS Azure yn darparu peiriant rhithwir gyda phrosesydd craidd 20 a 128 TB RAM am lai na $ 2 yr awr. Yn anffodus, mae'n amhosibl echdynnu'r uchafswm a harneisio'r pŵer hwn heb allu rheoli rhyngweithiad edafedd.

Terminoleg

Proses - Mae gwrthrych OS, gofod cyfeiriad ynysig, yn cynnwys edafedd.
Edau - gwrthrych OS, yr uned gyflawni leiaf, rhan o broses, edafedd yn rhannu cof ac adnoddau eraill ymhlith ei gilydd o fewn proses.
Amldasgio - OS eiddo, y gallu i redeg nifer o brosesau ar yr un pryd
Aml-graidd - un o briodweddau'r prosesydd, y gallu i ddefnyddio sawl craidd ar gyfer prosesu data
Amlbrosesu - yn eiddo i gyfrifiadur, y gallu i weithio ar yr un pryd â sawl prosesydd yn gorfforol
Aml-edau — un o briodweddau proses, y gallu i ddosbarthu prosesu data ymhlith sawl llinyn.
Parallelism - perfformio sawl gweithred gorfforol ar yr un pryd fesul uned o amser
Asynchrony - cyflawni gweithrediad heb aros i'r prosesu hwn gael ei gwblhau; gellir prosesu canlyniad y cyflawni yn ddiweddarach.

Sylwedd

Nid yw pob diffiniad yn dda ac mae angen esboniad ychwanegol ar rai, felly fe ychwanegaf drosiad am goginio brecwast at y derminoleg a gyflwynwyd yn ffurfiol. Mae coginio brecwast yn y trosiad hwn yn broses.

Wrth baratoi brecwast yn y bore dwi (CPU) Dw i'n dod i'r gegin (Cyfrifiadur). Mae gen i 2 law (Cores). Mae yna nifer o ddyfeisiau yn y gegin (IO): popty, tegell, tostiwr, oergell. Rwy'n troi'r nwy ymlaen, yn rhoi padell ffrio arno ac yn arllwys olew i mewn iddo heb aros iddo gynhesu (asynchronously, Di-Blocio-IO-Aros), Rwy'n cymryd yr wyau allan o'r oergell ac yn eu torri i mewn i blât, yna eu curo ag un llaw (Edau #1), ac yn ail (Edau #2) dal y plât (Adnodd a Rennir). Nawr hoffwn i droi ar y tegell, ond does gen i ddim digon o ddwylo (Newyn Edau) Yn ystod yr amser hwn, mae'r padell ffrio yn cynhesu (Prosesu'r canlyniad) ac rwy'n arllwys yr hyn rydw i wedi'i chwipio i mewn iddo. Rwy'n estyn am y tegell a'i droi ymlaen a gwylio'r dŵr yn berwi ynddo'n wirion (Blocio-IO-Aros), er y gallai yn ystod yr amser hwn fod wedi golchi'r plât lle bu'n chwipio'r omelet.

Fe wnes i goginio omled gan ddefnyddio dim ond 2 law, ac nid oes gen i fwy, ond ar yr un pryd, ar hyn o bryd o chwipio'r omled, cynhaliwyd 3 llawdriniaeth ar unwaith: chwipio'r omled, dal y plât, gwresogi'r padell ffrio ■ Y CPU yw rhan gyflymaf y cyfrifiadur, IO yw'r hyn sy'n fwyaf aml mae popeth yn arafu, felly yn aml ateb effeithiol yw meddiannu'r CPU gyda rhywbeth wrth dderbyn data gan IO.

Gan barhau â'r trosiad:

  • Os yn y broses o baratoi omelet, byddwn hefyd yn ceisio newid dillad, byddai hyn yn enghraifft o amldasgio. Naws bwysig: mae cyfrifiaduron yn llawer gwell am hyn na phobl.
  • Cegin gyda nifer o gogyddion, er enghraifft mewn bwyty - cyfrifiadur aml-graidd.
  • Mae llawer o fwytai mewn llys bwyd mewn canolfan siopa - canolfan ddata

Offer .NET

Mae .NET yn dda am weithio gydag edafedd, fel gyda llawer o bethau eraill. Gyda phob fersiwn newydd, mae'n cyflwyno mwy a mwy o offer newydd ar gyfer gweithio gyda nhw, haenau newydd o dynnu dros edafedd OS. Wrth weithio gydag adeiladu tyniadau, mae datblygwyr fframwaith yn defnyddio dull sy'n gadael y cyfle, wrth ddefnyddio tyniad lefel uchel, i fynd i lawr un neu fwy o lefelau islaw. Yn fwyaf aml nid yw hyn yn angenrheidiol, mewn gwirionedd mae'n agor y drws i saethu eich hun yn y droed gyda gwn saethu, ond weithiau, mewn achosion prin, efallai mai dyma'r unig ffordd i ddatrys problem nad yw'n cael ei datrys ar y lefel bresennol o dynnu. .

Wrth ddefnyddio offer, rwy'n golygu rhyngwynebau rhaglennu cymwysiadau (API) a ddarperir gan y fframwaith a phecynnau trydydd parti, yn ogystal ag atebion meddalwedd cyfan sy'n symleiddio'r chwilio am unrhyw broblemau sy'n ymwneud â chod aml-edau.

Dechrau edefyn

Y dosbarth Thread yw'r dosbarth mwyaf sylfaenol yn .NET ar gyfer gweithio gydag edafedd. Mae'r adeiladwr yn derbyn un o ddau gynrychiolydd:

  • ThreadStart - Dim paramedrau
  • ParametrizedThreadStart - gydag un paramedr o wrthrych math.

Bydd y dirprwy yn cael ei weithredu yn yr edefyn newydd ei greu ar ôl galw'r dull Cychwyn.Os cafodd dirprwy o'r math ParametrizedThreadStart ei drosglwyddo i'r adeiladwr, yna rhaid trosglwyddo gwrthrych i'r dull Cychwyn. Mae angen y mecanwaith hwn i drosglwyddo unrhyw wybodaeth leol i'r ffrwd. Mae'n werth nodi bod creu edau yn weithrediad drud, ac mae'r edau ei hun yn wrthrych trwm, o leiaf oherwydd ei fod yn dyrannu 1MB o gof ar y pentwr ac yn gofyn am ryngweithio â'r API OS.

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

Mae'r dosbarth ThreadPool yn cynrychioli'r cysyniad o bwll. Yn .NET, mae'r gronfa edau yn ddarn o beirianneg, ac mae'r datblygwyr yn Microsoft wedi gwneud llawer o ymdrech i sicrhau ei fod yn gweithio'n optimaidd mewn amrywiaeth eang o senarios.

Cysyniad cyffredinol:

O'r eiliad y bydd y cais yn cychwyn, mae'n creu sawl llinyn wrth gefn yn y cefndir ac yn darparu'r gallu i'w cymryd i'w defnyddio. Os defnyddir edafedd yn aml ac mewn niferoedd mawr, mae'r pwll yn ehangu i ddiwallu anghenion y galwr. Pan nad oes edafedd am ddim yn y pwll ar yr amser iawn, bydd naill ai'n aros i un o'r edafedd ddychwelyd, neu'n creu un newydd. Mae'n dilyn bod y pwll edau yn wych ar gyfer rhai camau gweithredu tymor byr ac yn addas iawn ar gyfer gweithrediadau sy'n rhedeg fel gwasanaethau trwy gydol gweithrediad cyfan y cais.

I ddefnyddio edefyn o'r pwll, mae yna ddull QueueUserWorkItem sy'n derbyn dirprwy o'r math WaitCallback, sydd â'r un llofnod â ParametrizedThreadStart, ac mae'r paramedr a drosglwyddir iddo yn cyflawni'r un swyddogaeth.

ThreadPool.QueueUserWorkItem(...);

Defnyddir y dull cronfa edau llai adnabyddus RegisterWaitForSingleObject i drefnu gweithrediadau IO nad yw'n rhwystro. Bydd y cynrychiolydd a drosglwyddir i'r dull hwn yn cael ei alw pan fydd y WaitHandle a drosglwyddir i'r dull yn cael ei “Rhyddhau”.

ThreadPool.RegisterWaitForSingleObject(...)

Mae gan .NET amserydd edau ac mae'n wahanol i amseryddion WinForms/WPF gan y bydd ei driniwr yn cael ei alw ar edefyn a gymerwyd o'r pwll.

System.Threading.Timer

Mae yna hefyd ffordd eithaf egsotig i anfon cynrychiolydd i'w ddienyddio i edefyn o'r pwll - y dull BeginInvoke.

DelegateInstance.BeginInvoke

Hoffwn ganolbwyntio'n fyr ar y swyddogaeth y gellir galw llawer o'r dulliau uchod iddi - CreateThread o Kernel32.dll Win32 API. Mae yna ffordd, diolch i fecanwaith dulliau allanol, i alw'r swyddogaeth hon. Dim ond unwaith yr wyf wedi gweld galwad o'r fath mewn enghraifft ofnadwy o god etifeddiaeth, ac mae cymhelliant yr awdur a wnaeth yn union hyn yn dal i fod yn ddirgelwch i mi.

Kernel32.dll CreateThread

Gweld a Dadfygio Trywyddau

Gellir gweld yr edafedd a grëwyd gennych chi, yr holl gydrannau trydydd parti, a'r pwll .NET yn ffenestr Threads yn Visual Studio. Bydd y ffenestr hon ond yn dangos gwybodaeth edau pan fydd y rhaglen o dan ddadfygio ac yn y modd Torri. Yma gallwch weld enwau stac a blaenoriaethau pob edefyn yn gyfleus, a newid dadfygio i edefyn penodol. Gan ddefnyddio eiddo Blaenoriaeth y dosbarth Thread, gallwch osod blaenoriaeth edefyn, y bydd yr OC a CLR yn ei weld fel argymhelliad wrth rannu amser prosesydd rhwng edafedd.

.NET: Offer ar gyfer gweithio gyda multithreading a asynchrony. Rhan 1

Llyfrgell Cyfochrog Tasg

Cyflwynwyd Task Parallel Library (TPL) yn .NET 4.0. Nawr dyma'r safon a'r prif offeryn ar gyfer gweithio gydag asynchrony. Ystyrir unrhyw god sy'n defnyddio dull hŷn yn etifeddiaeth. Uned sylfaenol TPL yw'r dosbarth Tasg o'r gofod enw System.Threading.Tasks. Tyniad dros edefyn yw tasg. Gyda'r fersiwn newydd o'r iaith C#, cawsom ffordd gain o weithio gyda Tasks - async / aros am weithredwyr. Roedd y cysyniadau hyn yn ei gwneud hi'n bosibl ysgrifennu cod asyncronig fel pe bai'n syml ac yn gydamserol, roedd hyn yn ei gwneud hi'n bosibl hyd yn oed i bobl heb lawer o ddealltwriaeth o weithrediad mewnol edafedd ysgrifennu cymwysiadau sy'n eu defnyddio, cymwysiadau nad ydynt yn rhewi wrth berfformio gweithrediadau hir. Mae defnyddio async/aros yn bwnc ar gyfer un neu hyd yn oed sawl erthygl, ond byddaf yn ceisio cael y gwir amdani mewn ychydig frawddegau:

  • Mae async yn addasydd dull sy'n dychwelyd Tasg neu wag
  • ac aros yn weithredwr aros Tasg nad yw'n rhwystro.

Unwaith eto: bydd y gweithredwr aros, yn yr achos cyffredinol (mae yna eithriadau), yn rhyddhau'r edefyn gweithredu cyfredol ymhellach, a phan fydd y Dasg yn gorffen ei gyflawni, a'r edefyn (mewn gwirionedd, byddai'n fwy cywir dweud y cyd-destun , ond bydd mwy am hynny yn ddiweddarach) yn parhau i weithredu'r dull ymhellach. Y tu mewn i .NET, gweithredir y mecanwaith hwn yn yr un modd â dychweliad cynnyrch, pan fydd y dull ysgrifenedig yn troi'n ddosbarth cyfan, sef peiriant cyflwr a gellir ei weithredu mewn darnau ar wahân yn dibynnu ar y cyflyrau hyn. Gall unrhyw un sydd â diddordeb ysgrifennu unrhyw god syml gan ddefnyddio asynс/aros, llunio a gweld y cynulliad gan ddefnyddio JetBrains dotPeek gyda'r Cod Cryno a Gynhyrchir wedi'i alluogi.

Gadewch i ni edrych ar opsiynau ar gyfer lansio a defnyddio Tasg. Yn yr enghraifft cod isod, rydym yn creu tasg newydd nad yw'n gwneud dim byd defnyddiol (Thread.Sleep(10000)), ond mewn bywyd go iawn dylai hyn fod yn waith cymhleth CPU-ddwys.

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
}

Mae Tasg yn cael ei chreu gyda nifer o opsiynau:

  • Mae LongRunning yn awgrym na fydd y dasg yn cael ei chwblhau'n gyflym, sy'n golygu y gallai fod yn werth ystyried peidio â chymryd edefyn o'r pwll, ond creu un ar wahân ar gyfer y Dasg hon er mwyn peidio â niweidio eraill.
  • AttachedToParent - Gellir trefnu tasgau mewn hierarchaeth. Pe bai'r opsiwn hwn yn cael ei ddefnyddio, yna efallai y bydd y Dasg mewn cyflwr lle mae wedi'i chwblhau ac yn aros am ddienyddiad ei phlant.
  • PreferFairness - yn golygu y byddai'n well cyflawni Tasgau a anfonwyd i'w cyflawni yn gynharach cyn y rhai a anfonir yn ddiweddarach. Ond argymhelliad yn unig yw hwn ac nid yw canlyniadau wedi'u gwarantu.

Yr ail baramedr a drosglwyddir i'r dull yw CancellationToken. Er mwyn delio'n gywir â chanslo gweithrediad ar ôl iddo ddechrau, rhaid llenwi'r cod sy'n cael ei weithredu â gwiriadau ar gyfer cyflwr CancellationToken. Os nad oes unrhyw wiriadau, yna dim ond cyn iddo ddechrau y bydd y dull Canslo a elwir ar y gwrthrych CancellationTokenSource yn gallu atal cyflawni'r Dasg.

Mae'r paramedr olaf yn wrthrych amserlennu o'r math TaskScheduler. Mae'r dosbarth hwn a'i ddisgynyddion wedi'u cynllunio i reoli strategaethau ar gyfer dosbarthu Tasgau ar draws edafedd; yn ddiofyn, bydd y Dasg yn cael ei chyflawni ar edefyn ar hap o'r pwll.

Mae'r gweithredwr aros yn cael ei gymhwyso i'r Dasg a grëwyd, sy'n golygu y bydd y cod a ysgrifennwyd ar ei ôl, os oes un, yn cael ei weithredu yn yr un cyd-destun (yn aml mae hyn yn golygu ar yr un edefyn) â'r cod cyn aros.

Mae'r dull wedi'i farcio fel gwagle async, sy'n golygu y gall ddefnyddio'r gweithredwr aros, ond ni fydd y cod galw yn gallu aros i'w weithredu. Os oes angen nodwedd o'r fath, yna rhaid i'r dull ddychwelyd Tasg. Mae dulliau a nodir yn wag async yn eithaf cyffredin: fel rheol, mae'r rhain yn ymdrin â digwyddiadau neu ddulliau eraill sy'n gweithio ar yr egwyddor tân ac anghofio. Os oes angen i chi nid yn unig roi'r cyfle i aros tan ddiwedd y gweithredu, ond hefyd dychwelyd y canlyniad, yna mae angen i chi ddefnyddio Tasg.

Ar y Dasg y dychwelodd y dull StartNew, yn ogystal ag ar unrhyw un arall, gallwch ffonio'r dull ConfigureAwait gyda'r paramedr ffug, yna bydd gweithredu ar ôl aros yn parhau nid ar y cyd-destun a ddaliwyd, ond ar un mympwyol. Dylid gwneud hyn bob amser pan nad yw'r cyd-destun gweithredu yn bwysig ar gyfer y cod ar ôl aros. Mae hwn hefyd yn argymhelliad gan MS wrth ysgrifennu cod a fydd yn cael ei ddosbarthu wedi'i becynnu mewn llyfrgell.

Gadewch i ni edrych ychydig mwy ar sut y gallwch chi aros i gwblhau Tasg. Isod mae enghraifft o god, gyda sylwadau ar pryd y caiff y disgwyliad ei wneud yn amodol yn dda a phryd y caiff ei wneud yn amodol yn wael.

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
}

Yn yr enghraifft gyntaf, rydym yn aros i'r Dasg ei chwblhau heb rwystro'r edefyn galw; dim ond pan fydd eisoes yno y byddwn yn dychwelyd i brosesu'r canlyniad; tan hynny, gadewir yr edefyn galw i'w ddyfeisiau ei hun.

Yn yr ail opsiwn, rydym yn rhwystro'r edefyn galw nes bod canlyniad y dull yn cael ei gyfrifo. Mae hyn yn ddrwg nid yn unig oherwydd ein bod wedi meddiannu edefyn, adnodd mor werthfawr o'r rhaglen, gyda segurdod syml, ond hefyd oherwydd os yw cod y dull yr ydym yn ei alw'n cynnwys yn aros, ac mae'r cyd-destun cydamseru yn gofyn am ddychwelyd i'r edefyn galw ar ôl aros, yna byddwn yn cael terfyn amser : Mae'r edefyn galw yn aros i ganlyniad y dull asyncronous gael ei gyfrifo, mae'r dull asyncronaidd yn ceisio yn ofer i barhau i weithredu yn y llinyn galw.

Anfantais arall y dull hwn yw trin gwallau cymhleth. Y ffaith yw bod gwallau yn y cod asyncronig wrth ddefnyddio async / await yn hawdd iawn i'w trin - maen nhw'n ymddwyn yr un fath â phe bai'r cod yn gydamserol. Tra os byddwn yn cymhwyso exorcism aros cydamserol i Dasg, mae'r eithriad gwreiddiol yn troi'n Eithriad Cyfunol, h.y. I ymdrin â'r eithriad, bydd yn rhaid i chi archwilio'r math InnerException ac ysgrifennu cadwyn os eich hun y tu mewn i un bloc dal neu ddefnyddio'r dalfa wrth adeiladu, yn lle'r gadwyn o flociau dal sy'n fwy cyfarwydd yn y byd C#.

Mae'r drydedd enghraifft a'r olaf hefyd wedi'u marcio'n wael am yr un rheswm ac yn cynnwys yr un problemau.

Mae’r dulliau WhenAny a WhenAll yn hynod o gyfleus ar gyfer aros am grŵp o Dasgau; maent yn lapio grŵp o Dasgau yn un, a fydd yn tanio naill ai pan fydd Tasg o’r grŵp yn cael ei sbarduno gyntaf, neu pan fydd pob un ohonynt wedi cwblhau eu cyflawni.

Atal edafedd

Am wahanol resymau, efallai y bydd angen atal y llif ar ôl iddo ddechrau. Mae yna nifer o ffyrdd o wneud hyn. Mae gan y dosbarth Thread ddau ddull a enwir yn briodol: Erthyliad и ymyriad. Nid yw'r un cyntaf yn cael ei argymell yn fawr i'w ddefnyddio, oherwydd ar ôl ei alw ar unrhyw adeg ar hap, yn ystod prosesu unrhyw gyfarwyddyd, bydd eithriad yn cael ei daflu ThreadAbortedException. Nid ydych yn disgwyl i eithriad o'r fath gael ei daflu wrth gynyddu unrhyw newidyn cyfanrif, iawn? Ac wrth ddefnyddio'r dull hwn, mae hon yn sefyllfa wirioneddol iawn. Os oes angen i chi atal y CLR rhag cynhyrchu eithriad o'r fath mewn adran benodol o'r cod, gallwch ei lapio mewn galwadau Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Mae unrhyw god a ysgrifennwyd mewn bloc terfynol yn cael ei lapio mewn galwadau o'r fath. Am y rheswm hwn, yn nyfnder y cod fframwaith gallwch ddod o hyd i flociau gyda chais gwag, ond nid gwag yn olaf. Mae Microsoft yn digalonni'r dull hwn gymaint fel na wnaethant ei gynnwys yn .net core.

Mae'r dull Interrupt yn gweithio'n fwy rhagweladwy. Gall dorri ar draws yr edefyn gydag eithriad ThreadInterruptedException dim ond yn ystod yr eiliadau hynny pan fydd yr edefyn mewn cyflwr aros. Mae'n mynd i mewn i'r cyflwr hwn wrth hongian wrth aros am WaitHandle, clo, neu ar ôl galw Thread.Sleep.

Mae'r ddau opsiwn a ddisgrifir uchod yn ddrwg oherwydd eu natur anrhagweladwy. Yr ateb yw defnyddio strwythur Tocyn Canslo a dosbarth CansloTokenSource. Y pwynt yw hyn: mae enghraifft o'r dosbarth CancellationTokenSource yn cael ei greu a dim ond yr un sy'n berchen arno all atal y llawdriniaeth trwy ffonio'r dull Diddymu. Dim ond y CancellationToken sy'n cael ei drosglwyddo i'r llawdriniaeth ei hun. Ni all perchnogion CancellationToken ganslo'r llawdriniaeth eu hunain, ond gallant ond wirio a yw'r llawdriniaeth wedi'i chanslo. Mae eiddo Boole ar gyfer hyn IsCancellationRequest a dull Cais ThrowIfCancel. Bydd yr olaf yn taflu eithriad TaskCanslo Eithriad pe bai'r dull Canslo yn cael ei alw ar yr enghraifft CancellationToken yn cael ei barrotio. A dyma'r dull yr wyf yn argymell ei ddefnyddio. Mae hyn yn welliant ar yr opsiynau blaenorol trwy gael rheolaeth lawn dros ba bryd y gellir rhoi'r gorau i weithrediad eithrio.

Yr opsiwn mwyaf creulon ar gyfer atal edefyn yw galw swyddogaeth TermminateThread API Win32. Gall ymddygiad y CLR ar ôl galw'r swyddogaeth hon fod yn anrhagweladwy. Ar MSDN mae'r canlynol wedi'u hysgrifennu am y swyddogaeth hon: “Mae TerminThread yn swyddogaeth beryglus na ddylid ond ei defnyddio yn yr achosion mwyaf eithafol. “

Trosi API etifeddiaeth yn Seiliedig ar Dasgau gan ddefnyddio dull FromAsync

Os ydych chi'n ddigon ffodus i weithio ar brosiect a ddechreuwyd ar ôl i Tasks gael ei gyflwyno ac a roddodd y gorau i achosi arswyd tawel i'r rhan fwyaf o ddatblygwyr, yna ni fydd yn rhaid i chi ddelio â llawer o hen APIs, rhai trydydd parti a rhai eich tîm wedi arteithio yn y gorffennol. Yn ffodus, bu tîm Fframwaith .NET yn gofalu amdanom, er efallai mai'r nod oedd gofalu amdanom ein hunain. Boed hynny ag y bo modd, mae gan .NET nifer o offer ar gyfer trosi cod yn ddi-boen a ysgrifennwyd mewn hen ddulliau rhaglennu asyncronaidd i'r un newydd. Un ohonynt yw'r dull FromAsync o TaskFactory. Yn yr enghraifft cod isod, rwy'n lapio hen ddulliau async y dosbarth WebRequest mewn Tasg gan ddefnyddio'r dull hwn.

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

Enghraifft yn unig yw hon ac mae'n annhebygol y bydd yn rhaid i chi wneud hyn gyda mathau adeiledig, ond mae unrhyw hen brosiect yn gyforiog o ddulliau BeginDoSomething sy'n dychwelyd dulliau IAsyncResult ac EndDoSomething sy'n ei dderbyn.

Trosi API etifeddiaeth i Dasg yn seiliedig ar ddosbarth TaskCompletionSource

Teclyn pwysig arall i'w ystyried yw'r dosbarth TasgCwblhauFfynhonnell. O ran swyddogaethau, pwrpas ac egwyddor gweithredu, efallai ei fod braidd yn atgoffa rhywun o ddull RegisterWaitForSingleObject y dosbarth ThreadPool, yr ysgrifennais amdano uchod. Gan ddefnyddio'r dosbarth hwn, gallwch chi lapio hen APIs asyncronig yn Nhasgau yn hawdd ac yn gyfleus.

Byddwch yn dweud fy mod eisoes wedi siarad am y dull FromAsync o'r dosbarth TaskFactory a fwriedir at y dibenion hyn. Yma bydd yn rhaid i ni gofio holl hanes datblygiad modelau asyncronaidd yn .net y mae Microsoft wedi'i gynnig dros y 15 mlynedd diwethaf: cyn y Patrwm Asyncronaidd yn Seiliedig ar Dasg (TAP), roedd y Patrwm Rhaglennu Asynchronous (APP), a oedd yn yn ymwneud â dulliau DechrauGwneud Rhywbeth yn dychwelyd Canlyniad IAsync a dulliau diweddDoRhywbeth sy'n ei dderbyn ac ar gyfer etifeddiaeth y blynyddoedd hyn mae'r dull FromAsync yn berffaith, ond dros amser, fe'i disodlwyd gan y Patrwm Asyncronaidd ar Sail Digwyddiad (EAP), a oedd yn tybio y byddai digwyddiad yn cael ei godi pan fyddai'r gweithrediad asynchronous wedi'i gwblhau.

Mae TaskCompletionSource yn berffaith ar gyfer lapio Tasks ac APIs etifeddiaeth sydd wedi'u hadeiladu o amgylch model y digwyddiad. Mae hanfod ei waith fel a ganlyn: mae gan wrthrych o'r dosbarth hwn eiddo cyhoeddus o'r math Tasg, y gellir rheoli ei gyflwr trwy ddulliau SetResult, SetException, ac ati yn y dosbarth TaskCompletionSource. Mewn mannau lle cafodd y gweithredwr aros ei gymhwyso i'r Dasg hon, bydd yn cael ei gyflawni neu'n methu ag eithriad yn dibynnu ar y dull a ddefnyddiwyd i'r TaskCompletionSource. Os nad yw'n glir o hyd, gadewch i ni edrych ar yr enghraifft cod hon, lle mae rhai hen API EAP wedi'i lapio mewn Tasg gan ddefnyddio Ffynhonnell TasgCwblhau: pan fydd y digwyddiad yn tanio, bydd y Dasg yn cael ei drosglwyddo i'r cyflwr Cwblhawyd, a'r dull a gymhwysodd y gweithredwr aros. Bydd y dasg hon yn ailddechrau ei chyflawni ar ôl derbyn y gwrthrych arwain.

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

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

    result completionSource.Task;
}

Awgrymiadau a Thriciau TaskCompletionSource

Nid lapio hen APIs yw'r cyfan y gellir ei wneud gan ddefnyddio TaskCompletionSource. Mae defnyddio'r dosbarth hwn yn agor posibilrwydd diddorol o ddylunio APIs amrywiol ar Dasgau nad ydynt yn llenwi edafedd. Ac mae'r ffrwd, fel y cofiwn, yn adnodd drud ac mae eu nifer yn gyfyngedig (yn bennaf gan faint o RAM). Gellir cyflawni'r cyfyngiad hwn yn hawdd trwy ddatblygu, er enghraifft, cymhwysiad gwe wedi'i lwytho â rhesymeg fusnes gymhleth. Gadewch i ni ystyried y posibiliadau yr wyf yn sôn amdanynt wrth weithredu tric o'r fath â Phleidlais Hir.

Yn fyr, hanfod y tric yw hyn: mae angen i chi dderbyn gwybodaeth gan yr API am rai digwyddiadau sy'n digwydd ar ei ochr, tra na all yr API, am ryw reswm, adrodd am y digwyddiad, ond dim ond y wladwriaeth y gall ddychwelyd. Enghraifft o'r rhain yw pob API a adeiladwyd ar ben HTTP cyn amseroedd WebSocket neu pan oedd yn amhosibl am ryw reswm i ddefnyddio'r dechnoleg hon. Gall y cleient ofyn i'r gweinydd HTTP. Ni all y gweinydd HTTP ei hun ddechrau cyfathrebu â'r cleient. Ateb syml yw pleidleisio'r gweinydd gan ddefnyddio amserydd, ond mae hyn yn creu llwyth ychwanegol ar y gweinydd ac oedi ychwanegol ar gyfartaledd TimerInterval / 2. I fynd o gwmpas hyn, dyfeisiwyd tric o'r enw Pleidleisio Hir, sy'n golygu gohirio'r ymateb gan y gweinydd nes i'r Goramser ddod i ben neu bydd digwyddiad yn digwydd. Os yw'r digwyddiad wedi digwydd, yna caiff ei brosesu, os na, yna anfonir y cais eto.

while(!eventOccures && !timeoutExceeded)  {

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

Ond bydd ateb o'r fath yn ofnadwy cyn gynted ag y bydd nifer y cleientiaid sy'n aros am y digwyddiad yn cynyddu, oherwydd ... Mae pob cleient o'r fath yn meddiannu llinyn cyfan yn aros am ddigwyddiad. Ydym, ac rydym yn cael oedi o 1ms ychwanegol pan fydd y digwyddiad yn cael ei sbarduno, gan amlaf nid yw hyn yn arwyddocaol, ond pam gwneud y feddalwedd yn waeth nag y gall fod? Os byddwn yn tynnu Thread.Sleep(1), yna yn ofer byddwn yn llwytho un craidd prosesydd 100% yn segur, gan gylchdroi mewn cylch diwerth. Gan ddefnyddio TaskCompletionSource gallwch chi ail-wneud y cod hwn yn hawdd a datrys yr holl broblemau a nodir uchod:

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

Nid yw'r cod hwn yn barod ar gyfer cynhyrchu, ond dim ond demo. Er mwyn ei ddefnyddio mewn achosion go iawn, mae angen i chi hefyd, o leiaf, ymdrin â'r sefyllfa pan fydd neges yn cyrraedd ar adeg pan nad oes neb yn ei ddisgwyl: yn yr achos hwn, dylai'r dull AsseptMessageAsync ddychwelyd Tasg sydd eisoes wedi'i chwblhau. Os mai dyma'r achos mwyaf cyffredin, yna gallwch chi feddwl am ddefnyddio ValueTask.

Pan fyddwn yn derbyn cais am neges, rydym yn creu ac yn gosod TaskCompletionSource yn y geiriadur, ac yna'n aros am yr hyn sy'n digwydd yn gyntaf: mae'r cyfnod amser penodedig yn dod i ben neu mae neges yn cael ei derbyn.

ValueTask: pam a sut

Mae'r gweithredwyr async / aros, fel y gweithredwr dychwelyd cynnyrch, yn cynhyrchu peiriant cyflwr o'r dull, a dyma greu gwrthrych newydd, nad yw bron bob amser yn bwysig, ond mewn achosion prin gall greu problem. Gall yr achos hwn fod yn ddull a elwir yn aml iawn, rydym yn sôn am ddegau a channoedd o filoedd o alwadau yr eiliad. Os yw dull o'r fath yn cael ei ysgrifennu yn y fath fodd fel ei fod yn y rhan fwyaf o achosion yn dychwelyd canlyniad gan osgoi'r holl ddulliau aros, yna mae .NET yn darparu offeryn i wneud y gorau o hyn - strwythur ValueTask. I'w gwneud yn glir, gadewch i ni edrych ar enghraifft o'i ddefnydd: mae storfa rydyn ni'n mynd iddi yn aml iawn. Mae yna rai gwerthoedd ynddo ac yna rydyn ni'n eu dychwelyd yn syml; os na, yna rydyn ni'n mynd i ryw IO araf i'w cael. Rwyf am wneud yr olaf yn asyncronig, sy'n golygu bod y dull cyfan yn troi allan i fod yn asyncronig. Felly, y ffordd amlwg o ysgrifennu'r dull yw fel a ganlyn:

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

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

Oherwydd yr awydd i wneud y gorau ychydig, ac ychydig o ofn o'r hyn y bydd Roslyn yn ei gynhyrchu wrth lunio'r cod hwn, gallwch ailysgrifennu'r enghraifft hon fel a ganlyn:

public Task<string> GetById(int id) {

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

Yn wir, yr ateb gorau posibl yn yr achos hwn fyddai gwneud y gorau o'r llwybr poeth, sef, cael gwerth o'r geiriadur heb unrhyw ddyraniadau diangen a llwyth ar y GC, tra yn yr achosion prin hynny pan fydd angen i ni fynd at IO am ddata o hyd. , bydd popeth yn parhau i fod yn fantais / minws yr hen ffordd:

public ValueTask<string> GetById(int id) {

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

Gadewch i ni edrych yn agosach ar y darn hwn o god: os oes gwerth yn y storfa, rydym yn creu strwythur, fel arall bydd y dasg go iawn yn cael ei lapio mewn un ystyrlon. Nid oes ots gan y cod galw ym mha lwybr y gweithredwyd y cod hwn: bydd ValueTask, o safbwynt cystrawen C#, yn ymddwyn yr un peth â Thasg arferol yn yr achos hwn.

TaskSchedulers: rheoli strategaethau lansio tasgau

Yr API nesaf yr hoffwn ei ystyried yw'r dosbarth TasgScheduler a'i deilliadau. Soniais eisoes uchod fod gan TPL y gallu i reoli strategaethau ar gyfer dosbarthu Tasgau ar draws edafedd. Diffinnir strategaethau o'r fath yn disgynyddion y dosbarth TaskScheduler. Gellir dod o hyd i bron unrhyw strategaeth y gallai fod ei hangen arnoch yn y llyfrgell. Estyniadau ParallelExtras, a ddatblygwyd gan Microsoft, ond nid yn rhan o .NET, ond wedi'i gyflenwi fel pecyn Nuget. Edrychwn yn fyr ar rai ohonynt:

  • CurrentThreadTaskScheduler — yn cyflawni Tasgau ar yr edefyn cyfredol
  • LimitedConcurrencyLevelTaskScheduler — yn cyfyngu ar nifer y Tasgau a gyflawnir ar yr un pryd gan baramedr N, a dderbynnir yn y lluniwr
  • ArchebwydTaskScheduler — wedi'i ddiffinio fel LimitedConcurrencyLevelTaskScheduler(1), felly bydd tasgau'n cael eu cyflawni'n ddilyniannol.
  • WorkStealingTaskScheduler — offer dwyn gwaith dull o ddosbarthu tasgau. Yn y bôn mae'n ThreadPool ar wahân. Yn datrys y broblem bod yn .NET ThreadPool yn ddosbarth statig, un ar gyfer pob cais, sy'n golygu y gall ei orlwytho neu ddefnydd anghywir mewn un rhan o'r rhaglen arwain at sgîl-effeithiau mewn rhan arall. Ar ben hynny, mae'n anodd iawn deall achos diffygion o'r fath. Hynny. Efallai y bydd angen defnyddio WorkStealingTaskSchedulers ar wahân mewn rhannau o'r rhaglen lle gallai'r defnydd o ThreadPool fod yn ymosodol ac yn anrhagweladwy.
  • QueuedTaskScheduler - yn caniatáu ichi gyflawni tasgau yn unol â rheolau ciw â blaenoriaeth
  • ThreadPerTaskScheduler — yn creu edefyn ar wahân ar gyfer pob Tasg a gyflawnir arno. Gall fod yn ddefnyddiol ar gyfer tasgau sy'n cymryd amser anrhagweladwy o hir i'w cwblhau.

Mae manwl dda erthygl am TaskSchedulers ar y blog microsoft.

Ar gyfer dadfygio cyfleus o bopeth sy'n ymwneud â Tasks, mae gan Visual Studio ffenestr Tasgau. Yn y ffenestr hon gallwch weld cyflwr presennol y dasg a neidio i'r llinell cod sy'n gweithredu ar hyn o bryd.

.NET: Offer ar gyfer gweithio gyda multithreading a asynchrony. Rhan 1

PLinq a'r dosbarth Parallel

Yn ogystal â Tasgau a phopeth a ddywedir amdanynt, mae dau declyn mwy diddorol yn .NET: PLinq (Linq2Parallel) a'r dosbarth Parallel. Mae'r cyntaf yn addo cyflawni holl weithrediadau Linq ar edau lluosog yn gyfochrog. Gellir ffurfweddu nifer yr edafedd gan ddefnyddio dull estyn WithDegreeOfParallelism. Yn anffodus, yn fwyaf aml nid oes gan PLinq yn ei fodd rhagosodedig ddigon o wybodaeth am fewnolion eich ffynhonnell ddata i ddarparu cynnydd cyflymder sylweddol, ar y llaw arall, mae'r gost o geisio yn isel iawn: does ond angen i chi ffonio'r dull AsParallel o'r blaen y gadwyn o ddulliau Linq a rhedeg profion perfformiad. Ar ben hynny, mae'n bosibl trosglwyddo gwybodaeth ychwanegol i PLinq am natur eich ffynhonnell ddata gan ddefnyddio'r mecanwaith Rhaniadau. Gallwch ddarllen mwy yma и yma.

Mae'r dosbarth statig Parallel yn darparu dulliau ar gyfer ailadrodd trwy gasgliad Foreach yn gyfochrog, gweithredu dolen For, a gweithredu cynrychiolwyr lluosog ochr yn ochr ag Invoke. Bydd gweithredu'r edefyn cyfredol yn cael ei atal nes bod y cyfrifiadau wedi'u cwblhau. Gellir ffurfweddu nifer yr edafedd trwy basio ParallelOptions fel y ddadl olaf. Gallwch hefyd nodi TaskScheduler a CancellationToken gan ddefnyddio opsiynau.

Canfyddiadau

Pan ddechreuais ysgrifennu'r erthygl hon yn seiliedig ar ddeunyddiau fy adroddiad a'r wybodaeth a gasglwyd gennyf yn ystod fy ngwaith ar ei ôl, nid oeddwn yn disgwyl y byddai cymaint ohono. Nawr, pan fydd y golygydd testun yr wyf yn teipio'r erthygl hon ynddo yn waradwyddus yn dweud wrthyf fod tudalen 15 wedi mynd, byddaf yn crynhoi'r canlyniadau interim. Ymdrinnir â thriciau eraill, APIs, offer gweledol a pheryglon yn yr erthygl nesaf.

Casgliadau:

  • Mae angen i chi wybod yr offer ar gyfer gweithio gydag edafedd, asyncronig a chyfochredd er mwyn defnyddio adnoddau cyfrifiaduron modern.
  • Mae gan .NET lawer o wahanol offer at y dibenion hyn
  • Nid oedd pob un ohonynt yn ymddangos ar unwaith, felly gallwch chi ddod o hyd i rai etifeddiaeth yn aml, fodd bynnag, mae yna ffyrdd i drosi hen APIs heb lawer o ymdrech.
  • Mae gweithio gydag edafedd yn .NET yn cael ei gynrychioli gan y dosbarthiadau Thread and ThreadPool
  • Mae'r dulliau Thread.Abort, Thread.Interrupt, a Win32 API TerminThread yn beryglus ac nid ydynt yn cael eu hargymell i'w defnyddio. Yn lle hynny, mae'n well defnyddio'r mecanwaith CancellationToken
  • Mae llif yn adnodd gwerthfawr ac mae ei gyflenwad yn gyfyngedig. Dylid osgoi sefyllfaoedd lle mae edafedd yn brysur yn aros am ddigwyddiadau. Ar gyfer hyn mae'n gyfleus defnyddio'r dosbarth TaskCompletionSource
  • Tasgau yw'r offer .NET mwyaf pwerus ac uwch ar gyfer gweithio gyda chyfochrogrwydd ac asyncroni.
  • Mae'r gweithredwyr c# async/aros yn gweithredu'r cysyniad o aros heb rwystro
  • Gallwch reoli dosbarthiad Tasgau ar draws edafedd gan ddefnyddio dosbarthiadau sy'n deillio o TaskScheduler
  • Gall strwythur ValueTask fod yn ddefnyddiol wrth optimeiddio llwybrau poeth a thraffig cof
  • Mae ffenestri Tasgau a Threads Visual Studio yn darparu llawer o wybodaeth ddefnyddiol ar gyfer dadfygio cod aml-edau neu asyncronig
  • Mae PLinq yn offeryn cŵl, ond efallai nad oes ganddo ddigon o wybodaeth am eich ffynhonnell ddata, ond gellir ei drwsio gan ddefnyddio'r mecanwaith rhannu
  • I'w barhau…

Ffynhonnell: hab.com

Ychwanegu sylw