.NET: Verkfæri til að vinna með fjölþráða og ósamstillingu. 1. hluti

Ég er að birta upprunalegu greinina á Habr, þýðing hennar er birt í fyrirtækinu bloggfærsla.

Þörfin fyrir að gera eitthvað ósamstillt, án þess að bíða eftir niðurstöðunni hér og nú, eða að skipta stóru starfi á nokkrar einingar sem framkvæma það, var fyrir tilkomu tölvunnar. Með tilkomu þeirra varð þessi þörf mjög áþreifanleg. Nú, árið 2019, er ég að skrifa þessa grein á fartölvu með 8 kjarna Intel Core örgjörva, þar sem meira en eitt hundrað ferlar eru í gangi samhliða, og jafnvel fleiri þræði. Nálægt er svolítið subbulegur sími, keyptur fyrir nokkrum árum, hann er með 8 kjarna örgjörva um borð. Þemaefni eru full af greinum og myndböndum þar sem höfundar þeirra dást að flaggskipssnjallsímum þessa árs sem eru með 16 kjarna örgjörva. MS Azure útvegar sýndarvél með 20 kjarna örgjörva og 128 TB vinnsluminni fyrir minna en $2/klst. Því miður er ómögulegt að ná hámarkinu og virkja þennan kraft án þess að geta stjórnað samspili þráða.

Terminology

Ferli - OS hlutur, einangrað heimilisfangarými, inniheldur þræði.
Þráður - OS hlutur, minnsta framkvæmdareining, hluti af ferli, þræðir deila minni og öðrum auðlindum sín á milli innan ferlis.
Fjölverkavinnsla - OS eign, hæfni til að keyra nokkra ferla samtímis
Fjölkjarna - eiginleiki örgjörvans, hæfileikinn til að nota nokkra kjarna til gagnavinnslu
Fjölvinnsla - eiginleiki tölvu, getu til að vinna samtímis með nokkrum örgjörvum líkamlega
Fjölþráður — eiginleiki ferlis, hæfileikinn til að dreifa gagnavinnslu á nokkra þræði.
Hliðstæður - framkvæma nokkrar aðgerðir líkamlega samtímis á hverja tímaeiningu
Ósamstilling — framkvæmd aðgerðar án þess að bíða eftir að þessari vinnslu ljúki; hægt er að vinna úr niðurstöðu framkvæmdarinnar síðar.

Metafor

Ekki eru allar skilgreiningar góðar og sumar þurfa frekari skýringar, svo ég bæti myndlíkingu um að elda morgunmat við formlega kynnt hugtök. Að elda morgunmat í þessari myndlíkingu er ferli.

Þegar ég undirbjó morgunmat á morgnana ég (CPU) Ég kem í eldhúsið (Computer). ég er með 2 hendur (Algerlega). Það er fjöldi tækja í eldhúsinu (IO): ofn, ketill, brauðrist, ísskápur. Ég kveiki á gasinu, set pönnu á hana og helli olíu í hana án þess að bíða eftir að hún hitni (ósamstilltur, Non-Blocking-IO-Bíddu), ég tek eggin úr kæliskápnum og brýt þau í disk og þeyti þeim síðan með annarri hendi (Þráður #1), og annað (Þráður #2) halda á disknum (Shared Resource). Nú langar mig að kveikja á katlinum, en ég hef ekki nógu margar hendur (Þráður Hungursneyð) Á þessum tíma hitnar steikarpannan (Unnur afraksturinn) sem ég helli því sem ég hef þeytt í. Ég teygi mig í katlinum og kveiki á honum og horfi heimskulega á vatnið sjóða í honum (Útilokun-IO-Bíddu), þó að á þessum tíma hefði hann getað þvegið diskinn þar sem hann þeytti eggjakökuna.

Ég eldaði eggjaköku með aðeins 2 höndum, og ég á ekki fleiri, en á sama tíma, á því augnabliki sem eggjakakan var þeytt, fóru fram 3 aðgerðir í einu: þeyta eggjakökuna, halda á disknum, hita pönnuna Örgjörvinn er hraðskreiðasti hluti tölvunnar, IO er það sem er oftast allt hægir á sér, svo oft er áhrifarík lausn að taka örgjörvann upp með einhverju á meðan þú tekur á móti gögnum frá IO.

Áfram myndlíkingunni:

  • Ef í því ferli að útbúa eggjaköku myndi ég líka reyna að skipta um föt, þá væri þetta dæmi um fjölverkavinnsla. Mikilvægur blæbrigði: tölvur eru miklu betri í þessu en fólk.
  • Eldhús með nokkrum kokkum, til dæmis á veitingastað - fjölkjarna tölva.
  • Margir veitingastaðir í matarsal í verslunarmiðstöð - gagnaver

.NET verkfæri

.NET er gott að vinna með þræði eins og með margt annað. Með hverri nýrri útgáfu kynnir það fleiri og fleiri ný verkfæri til að vinna með þau, ný lög af abstrakt yfir OS þræði. Þegar unnið er með smíði abstrakta, nota rammahönnuðir nálgun sem gefur tækifæri til að fara niður eitt eða fleiri stig fyrir neðan þegar þeir nota háþróaða abstrakt. Oftast er þetta ekki nauðsynlegt, í raun opnar það dyrnar að skjóta sjálfan sig í fótinn með haglabyssu, en stundum, í einstaka tilfellum, getur það verið eina leiðin til að leysa vandamál sem er ekki leyst á núverandi abstraktstigi .

Með verkfærum á ég við bæði forritunarviðmót (API) sem rammakerfið býður upp á og þriðja aðila pakka, sem og heilar hugbúnaðarlausnir sem einfalda leitina að vandamálum sem tengjast fjölþráðum kóða.

Að stofna þráð

Þráðarflokkurinn er grunnklasinn í .NET til að vinna með þræði. Smiðurinn tekur við einum af tveimur fulltrúum:

  • ThreadStart - Engar breytur
  • ParametrizedThreadStart - með einni færibreytu af gerðinni hlut.

Fulltrúinn verður keyrður í nýstofnaða þræðinum eftir að hafa kallað á Start aðferðina. Ef fulltrúa af gerðinni ParametrizedThreadStart var send til byggingaraðilans, þá verður hlutur að fara í Start aðferðina. Þessi vélbúnaður er nauðsynlegur til að flytja allar staðbundnar upplýsingar í strauminn. Þess má geta að það er dýr aðgerð að búa til þráð og þráðurinn sjálfur er þungur hlutur, að minnsta kosti vegna þess að hann úthlutar 1MB af minni á stafla og krefst samskipta við OS API.

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

ThreadPool flokkurinn táknar hugmyndina um sundlaug. Í .NET er þráðasafnið verkfræðiverk og þróunaraðilar hjá Microsoft hafa lagt mikið á sig til að tryggja að það virki sem best í margvíslegum aðstæðum.

Almennt hugtak:

Frá því augnabliki sem forritið byrjar, býr það til nokkra þræði í varasjóði í bakgrunni og veitir möguleika á að taka þá til notkunar. Ef þræðir eru notaðir oft og í miklu magni stækkar sundlaugin til að mæta þörfum þess sem hringir. Þegar engir lausir þræðir eru í lauginni á réttum tíma mun hún annaðhvort bíða eftir að einn af þráðunum skili sér eða búa til nýjan. Af þessu leiðir að þráðasafnið er frábært fyrir sumar skammtímaaðgerðir og hentar illa fyrir aðgerðir sem keyra sem þjónusta í öllu rekstri forritsins.

Til að nota þráð úr lauginni er QueueUserWorkItem aðferð sem tekur við fulltrúa af gerðinni WaitCallback, sem hefur sömu undirskrift og ParametrizedThreadStart, og færibreytan sem send er til hans framkvæmir sömu aðgerðina.

ThreadPool.QueueUserWorkItem(...);

Minna þekkta þráðasafnsaðferðin RegisterWaitForSingleObject er notuð til að skipuleggja IO aðgerðir sem ekki hindrar. Hringt verður í fulltrúann sem er sendur í þessa aðferð þegar WaitHandle sem er sent til aðferðarinnar er „Loft“.

ThreadPool.RegisterWaitForSingleObject(...)

.NET er með þráðatímamæli og er hann frábrugðinn WinForms/WPF tímamælum að því leyti að meðhöndlari hans verður kallaður á þráð sem tekinn er úr lauginni.

System.Threading.Timer

Það er líka frekar framandi leið til að senda fulltrúa til framkvæmdar á þráð úr lauginni - BeginInvoke aðferðin.

DelegateInstance.BeginInvoke

Mig langar að staldra stuttlega við aðgerðina sem hægt er að kalla margar af ofangreindum aðferðum til - CreateThread frá Kernel32.dll Win32 API. Það er leið, þökk sé vélbúnaði ytri aðferða, til að kalla þessa aðgerð. Ég hef aðeins einu sinni séð slíkt símtal í hræðilegu dæmi um arfleifð kóða, og hvatning höfundarins sem gerði nákvæmlega þetta er mér enn ráðgáta.

Kernel32.dll CreateThread

Skoða og kemba þræði

Hægt er að skoða þræði sem þú hefur búið til, alla íhluti þriðja aðila og .NET hópinn í þræðiglugganum í Visual Studio. Þessi gluggi mun aðeins sýna þráðaupplýsingar þegar forritið er í villuleit og í hléstillingu. Hér geturðu á þægilegan hátt skoðað staflanöfn og forgangsröðun hvers þráðs og skipt um villuleit yfir á ákveðinn þráð. Með því að nota forgangseiginleika Thread classsins geturðu stillt forgang þráðs, sem OC og CLR munu skynja sem tilmæli þegar skipt er örgjörvatíma á milli þráða.

.NET: Verkfæri til að vinna með fjölþráða og ósamstillingu. 1. hluti

Task Parallel Library

Task Parallel Library (TPL) var kynnt í .NET 4.0. Nú er það staðallinn og aðalverkfærið til að vinna með ósamstillingu. Sérhver kóði sem notar eldri nálgun er talinn arfur. Grunneining TPL er Task class frá System.Threading.Tasks nafnrýminu. Verkefni er útdráttur yfir þráð. Með nýju útgáfunni af C# tungumálinu fengum við glæsilega leið til að vinna með Tasks - ósamstillt/bíður rekstraraðila. Þessi hugtök gerðu það að verkum að hægt væri að skrifa ósamstilltan kóða eins og hann væri einfaldur og samstilltur, þetta gerði jafnvel fólki með lítinn skilning á innri virkni þráða kleift að skrifa forrit sem nota þá, forrit sem frýs ekki þegar langvarandi aðgerðir eru framkvæmdar. Að nota async/wait er efni fyrir eina eða jafnvel nokkrar greinar, en ég mun reyna að fá kjarnann í því í nokkrum setningum:

  • ósamstilling er breyting á aðferð sem skilar Verkefni eða ógildu
  • and await er ólokandi verkefni sem bíður.

Enn og aftur: biðstjórinn, í almennu tilviki (það eru undantekningar), mun gefa út núverandi þráð um framkvæmd frekar, og þegar verkefninu lýkur framkvæmd sinni, og þráðinn (í raun væri réttara að segja samhengið , en meira um það síðar) mun halda áfram að framkvæma aðferðina frekar. Inni í .NET er þetta kerfi útfært á sama hátt og ávöxtunarkrafa, þegar skrifaða aðferðin breytist í heilan flokk, sem er ástandsvél og hægt er að keyra hana í aðskildum hlutum eftir þessum ríkjum. Allir sem hafa áhuga geta skrifað hvaða einfaldan kóða sem er með asynс/wait, safnað saman og skoðað samsetninguna með því að nota JetBrains dotPeek með þýðandamyndaðan kóða virkan.

Við skulum skoða valkosti til að ræsa og nota Task. Í kóðadæminu hér að neðan búum við til nýtt verkefni sem gerir ekkert gagnlegt (Þráður.Svefn(10000)), en í raunveruleikanum ætti þetta að vera flókin CPU-frek vinna.

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
}

Verkefni er búið til með fjölda valkosta:

  • LongRunning er vísbending um að verkefnið verði ekki klárað fljótt, sem þýðir að það gæti verið þess virði að íhuga að taka ekki þráð úr lauginni, heldur búa til sérstakan þráð fyrir þetta verkefni til að skaða ekki aðra.
  • AttachedToParent - Verkefnum er hægt að raða í stigveldi. Ef þessi valkostur var notaður gæti verkefnið verið í því ástandi að það hafi sjálft lokið og bíður eftir aftöku barna sinna.
  • PreferFairness - þýðir að betra væri að framkvæma verkefni sem send eru til framkvæmdar fyrr en þau sem send eru síðar. En þetta er bara tilmæli og árangur er ekki tryggður.

Önnur færibreytan sem send er til aðferðarinnar er CancellationToken. Til að meðhöndla afturköllun aðgerðar á réttan hátt eftir að hún hefur byrjað, verður að fylla út kóðann sem er keyrður með ávísunum á CancellationToken ástandið. Ef það eru engar athuganir, þá mun Cancell aðferðin sem kallað er á CancellationTokenSource hlutinn geta stöðvað framkvæmd verkefnisins aðeins áður en það byrjar.

Síðasta færibreytan er tímaáætlunarhlutur af gerðinni TaskScheduler. Þessi flokkur og afkomendur hans eru hannaðir til að stjórna aðferðum til að dreifa verkefnum yfir þræði; sjálfgefið verður verkefnið keyrt á handahófskenndum þræði úr hópnum.

Beðið stjórnandi er notaður á stofnað verkefni, sem þýðir að kóðinn sem skrifaður er á eftir því, ef hann er til, verður keyrður í sama samhengi (oft þýðir þetta á sama þræði) og kóðinn á undan bíður.

Aðferðin er merkt sem ósamstilltur ógildur, sem þýðir að hún getur notað biðstjórann, en hringingarkóði mun ekki geta beðið eftir framkvæmd. Ef slíkur eiginleiki er nauðsynlegur, þá verður aðferðin að skila Verkefni. Aðferðir sem merktar eru ósamstilltar ógildar eru nokkuð algengar: að jafnaði eru þetta atburðastjórnunarmenn eða aðrar aðferðir sem vinna á eld- og gleymskureglunni. Ef þú þarft ekki aðeins að gefa tækifæri til að bíða til loka framkvæmdar, heldur einnig skila niðurstöðunni, þá þarftu að nota Task.

Í verkefninu sem StartNew aðferðin skilaði, sem og á hverri annarri, geturðu kallað ConfigureAwait aðferðina með rangri breytu, þá mun framkvæmd eftir await halda áfram, ekki á handteknu samhengi, heldur á handahófskenndu samhengi. Þetta ætti alltaf að gera þegar framkvæmdarsamhengið er ekki mikilvægt fyrir kóðann eftir bið. Þetta eru líka tilmæli frá MS þegar þú skrifar kóða sem verður afhentur pakkaður á bókasafn.

Við skulum dvelja aðeins meira í því hvernig þú getur beðið eftir að verkefninu sé lokið. Hér að neðan er dæmi um kóða, með athugasemdum um hvenær væntingin er skilyrt vel gerð og hvenær hún er skilyrt illa gerð.

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
}

Í fyrsta dæminu bíðum við eftir að verkefninu ljúki án þess að loka á hringingarþráðinn; við munum snúa aftur til að vinna úr niðurstöðunni aðeins þegar hún er þegar til staðar; þangað til er hringingarþráðurinn eftir í eigin tækjum.

Í seinni valkostinum lokum við á kallþráðinn þar til niðurstaða aðferðarinnar er reiknuð út. Þetta er slæmt, ekki aðeins vegna þess að við höfum upptekið þráð, svo dýrmæta auðlind forritsins, með einfaldri aðgerðaleysi, heldur einnig vegna þess að ef kóðinn á aðferðinni sem við köllum inniheldur bíður, og samstillingarsamhengið krefst þess að fara aftur í kallþráðinn eftir bíddu, þá fáum við kyrrstöðu : Kallþráðurinn bíður eftir að niðurstaða ósamstilltu aðferðarinnar verði reiknuð út, ósamstilltu aðferðin reynir árangurslaust að halda áfram framkvæmd sinni í köllunarþræðinum.

Annar ókostur við þessa nálgun er flókin villumeðferð. Staðreyndin er sú að villur í ósamstilltum kóða þegar þú notar async/await er mjög auðvelt að meðhöndla - þær hegða sér eins og ef kóðinn væri samstilltur. Þó að ef við beittum samstilltum biðútdrætti á verkefni, þá breytist upprunalega undantekningin í AggregateException, þ.e. Til að takast á við undantekninguna þarftu að skoða InnerException tegundina og skrifa ef keðja sjálfan þig inni í einum catch blokk eða nota aflann þegar þú ert smíðuð, í stað keðjunnar af catch blokkum sem er þekktari í C# heiminum.

Þriðja og síðasta dæmið eru einnig merkt slæm af sömu ástæðu og innihalda öll sömu vandamálin.

WhenAny og WhenAll aðferðirnar eru einstaklega hentugar til að bíða eftir hópi af verkefnum; þær vefja hóp af verkefnum í eitt, sem ræsir annað hvort þegar verkefni frá hópnum er fyrst ræst eða þegar þau hafa öll lokið framkvæmd sinni.

Stöðva þræði

Af ýmsum ástæðum getur verið nauðsynlegt að stöðva flæðið eftir að það hefur byrjað. Það eru nokkrar leiðir til að gera þetta. Þráðarflokkurinn hefur tvær aðferðir sem heita viðeigandi: Brottfall и Truflaðu. Það fyrsta er ekki mælt með notkun vegna þess eftir að hafa hringt í það á hvaða augnabliki sem er af handahófi, meðan á vinnslu hvers kyns leiðbeiningar stendur, verður undantekning hent ThreadAbortedException. Þú býst ekki við að slík undantekning verði hent þegar þú hækkar einhverja heiltölubreytu, ekki satt? Og þegar þessi aðferð er notuð er þetta mjög raunverulegt ástand. Ef þú þarft að koma í veg fyrir að CLR framkalli slíka undantekningu í ákveðnum hluta kóðans geturðu sett það inn í símtöl Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Sérhver kóði sem skrifaður er í loksins blokk er pakkaður inn í slík símtöl. Af þessum sökum, í djúpum rammakóðans, geturðu fundið kubba með tómri tilraun, en ekki tómri að lokum. Microsoft dregur úr þessari aðferð svo mikið að það var ekki með hana í .net kjarna.

Interrupt aðferðin virkar fyrirsjáanlegri. Það getur truflað þráðinn með undantekningu Thread InterruptedException aðeins á þeim augnablikum þegar þráðurinn er í biðstöðu. Það fer í þetta ástand á meðan það hangir á meðan beðið er eftir WaitHandle, læsingu eða eftir að hafa hringt í Thread.Sleep.

Báðir valkostir sem lýst er hér að ofan eru slæmir vegna ófyrirsjáanleika þeirra. Lausnin er að nota mannvirki CancellationToken og bekk CancellationTokenSource. Málið er þetta: tilvik af CancellationTokenSource bekknum er búið til og aðeins sá sem á hann getur stöðvað aðgerðina með því að kalla aðferðina Hætta. Aðeins CancellationToken er sent til aðgerðarinnar sjálfrar. CancellationToken eigendur geta ekki hætt við aðgerðina sjálfir, heldur geta aðeins athugað hvort aðgerðin hafi verið hætt. Það er Boolean eign fyrir þetta IsCancellationRequested og aðferð ThrowIfCancelRequested. Hið síðarnefnda mun kasta undantekningu TaskCancelledException ef Cancel aðferðin var kölluð á CancellationToken tilvikinu sem verið er að parrota. Og þetta er aðferðin sem ég mæli með að nota. Þetta er framför frá fyrri valkostum með því að ná fullri stjórn á því hvenær hægt er að hætta við undantekningaraðgerð.

Hrikalegasti kosturinn til að stöðva þráð er að kalla á Win32 API TerminateThread aðgerðina. Hegðun CLR eftir að hafa kallað þessa aðgerð getur verið ófyrirsjáanleg. Á MSDN er eftirfarandi skrifað um þessa aðgerð: „TerminateThread er hættuleg aðgerð sem ætti aðeins að nota í erfiðustu tilfellum. “

Umbreytir eldri API í Task Based með FromAsync aðferð

Ef þú ert svo heppinn að vinna að verkefni sem byrjað var eftir að Tasks voru kynnt og hætti að valda rólegum hryllingi fyrir flesta forritara, þá þarftu ekki að takast á við fullt af gömlum API, bæði þriðja aðila og teyminu þínu hefur pyntað í fortíðinni. Sem betur fer sá .NET Framework teymið um okkur, þó markmiðið hafi kannski verið að sjá um okkur sjálf. Hvað sem því líður þá er .NET með fjölda verkfæra til að umbreyta kóða sem skrifaður er í gömlum ósamstilltri forritunaraðferðum á sársaukalausan hátt yfir í þann nýja. Ein þeirra er FromAsync aðferð TaskFactory. Í kóðadæminu hér að neðan pakka ég gömlu ósamstillingaraðferðunum í WebRequest bekknum inn í verkefni með þessari aðferð.

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

Þetta er bara dæmi og ólíklegt að þú þurfir að gera þetta með innbyggðum týpum, en hvaða gamalt verkefni sem er er einfaldlega fullt af BeginDoSomething aðferðum sem skila IAsyncResult og EndDoSomething aðferðum sem taka við því.

Umbreyttu eldri API í Task Based með TaskCompletionSource flokki

Annað mikilvægt tæki til að íhuga er bekkurinn TaskCompletionSource. Hvað varðar aðgerðir, tilgang og aðgerðareglu, þá gæti það minnt nokkuð á RegisterWaitForSingleObject aðferðina í ThreadPool bekknum, sem ég skrifaði um hér að ofan. Með því að nota þennan flokk geturðu auðveldlega og þægilega pakkað gömlum ósamstilltum API inn í Verkefni.

Þú munt segja að ég hafi þegar talað um FromAsync aðferðina í TaskFactory bekknum sem ætlað er í þessum tilgangi. Hér verðum við að muna alla sögu þróunar ósamstilltra gerða í .net sem Microsoft hefur boðið upp á undanfarin 15 ár: áður en Task-Based Asynchronous Pattern (TAP) var til ósamstilltur forritunarmynstur (APP), sem var um aðferðir ByrjaðuGera Eitthvað aftur IAsyncResult og aðferðir EndaDoSomething sem samþykkir það og fyrir arfleifð þessara ára er FromAsync aðferðin bara fullkomin, en með tímanum var henni skipt út fyrir Event Based Unsynchronous Pattern (OG AP), sem gerði ráð fyrir að atburður myndi koma upp þegar ósamstilltu aðgerðinni væri lokið.

TaskCompletionSource er fullkomið til að vefja verkefni og eldri API sem eru byggð í kringum viðburðarlíkanið. Kjarninn í vinnu þess er sem hér segir: hlutur í þessum flokki hefur opinberan eiginleika af gerðinni Task, sem hægt er að stjórna með aðferðum SetResult, SetException o.s.frv. TaskCompletionSource flokksins. Á stöðum þar sem biðstjórnandinn var notaður á þetta verkefni, verður það keyrt eða mistókst með undantekningu eftir því hvernig aðferðin er notuð á TaskCompletionSource. Ef það er enn ekki ljóst skulum við líta á þetta kóðadæmi, þar sem eitthvert gamalt EAP API er vafinn inn í verkefni með því að nota TaskCompletionSource: þegar atburðurinn kviknar verður verkefnið flutt yfir í Lokið ástand, og aðferðina sem beitti biðstjóranum til þessa verkefnis mun halda áfram framkvæmd sinni eftir að hafa fengið hlutinn leitt.

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

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

    result completionSource.Task;
}

TaskCompletionSource Ábendingar og brellur

Það er ekki allt sem hægt er að gera með því að nota TaskCompletionSource að vefja gömul API. Notkun þessa flokks opnar áhugaverðan möguleika á að hanna ýmis API fyrir verkefni sem taka ekki þræði. Og straumurinn, eins og við munum, er dýr auðlind og fjöldi þeirra er takmarkaður (aðallega af magni vinnsluminni). Þessari takmörkun er auðvelt að ná með því að þróa, til dæmis, hlaðið vefforrit með flókinni viðskiptarökfræði. Við skulum íhuga möguleikana sem ég er að tala um þegar þú innleiðir svona brellu eins og Long-Polling.

Í stuttu máli er kjarninn í bragðinu þessi: þú þarft að fá upplýsingar frá API um suma atburði sem eiga sér stað á hliðinni, á meðan API, af einhverjum ástæðum, getur ekki tilkynnt um atburðinn, heldur getur aðeins skilað ástandinu. Dæmi um þetta eru öll API sem byggð voru ofan á HTTP fyrir tíma WebSocket eða þegar það var ómögulegt af einhverjum ástæðum að nota þessa tækni. Viðskiptavinurinn getur spurt HTTP netþjóninn. HTTP þjónninn getur ekki sjálfur hafið samskipti við biðlarann. Einföld lausn er að kanna netþjóninn með því að nota tímamæli en það skapar aukið álag á netþjóninn og auka seinkun að meðaltali TimerInterval / 2. Til að komast framhjá þessu var fundið upp bragð sem kallast Long Polling sem felur í sér að seinka svari frá kl. þjóninn þar til tímamörk rennur út eða atburður mun eiga sér stað. Ef atburðurinn hefur átt sér stað, þá er hann afgreiddur, ef ekki, þá er beiðnin send aftur.

while(!eventOccures && !timeoutExceeded)  {

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

En slík lausn mun reynast hræðileg um leið og fjöldi viðskiptavina sem bíða eftir viðburðinum fjölgar, því... Hver slíkur viðskiptavinur tekur heilan þráð sem bíður eftir atburði. Já, og við fáum 1 ms seinkun til viðbótar þegar atburðurinn er settur af stað, oftast er þetta ekki marktækt, en hvers vegna gera hugbúnaðinn verri en hann getur verið? Ef við fjarlægjum Thread.Sleep(1), þá til einskis munum við hlaða einum örgjörvakjarna 100% aðgerðalausum, snúast í gagnslausri lotu. Með því að nota TaskCompletionSource geturðu auðveldlega endurgert þennan kóða og leyst öll vandamálin sem tilgreind eru hér að ofan:

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

Þessi kóði er ekki tilbúinn til framleiðslu heldur bara kynningu. Til að nota það í raunverulegum tilfellum þarftu líka að minnsta kosti að takast á við aðstæður þegar skilaboð berast á þeim tíma sem enginn á von á því: í þessu tilviki ætti AsseptMessageAsync aðferðin að skila þegar lokið verkefni. Ef þetta er algengasta tilvikið geturðu hugsað þér að nota ValueTask.

Þegar við fáum beiðni um skilaboð búum við til og setjum TaskCompletionSource í orðabókina og bíðum síðan eftir því sem gerist fyrst: tilgreint tímabil rennur út eða skilaboð berast.

ValueTask: hvers vegna og hvernig

Ósamstilltu/bíður rekstraraðilar, eins og rekstraraðili ávöxtunarskila, búa til ástandsvél úr aðferðinni og þetta er sköpun nýs hlutar, sem er nánast alltaf mikilvægt, en í einstaka tilfellum getur það skapað vandamál. Þetta tilfelli getur verið aðferð sem er mjög oft kölluð, við erum að tala um tugi og hundruð þúsunda símtala á sekúndu. Ef slík aðferð er skrifuð á þann hátt að hún skilar í flestum tilfellum niðurstöðu sem gengur framhjá öllum biðaðferðum, þá gefur .NET tæki til að hagræða þessu - ValueTask uppbyggingu. Til að gera það ljóst skulum við skoða dæmi um notkun þess: það er skyndiminni sem við förum mjög oft í. Það eru nokkur gildi í því og þá skilum við þeim einfaldlega; ef ekki, þá förum við í hægan IO til að ná þeim. Ég vil gera hið síðarnefnda ósamstillt, sem þýðir að öll aðferðin reynist vera ósamstillt. Þannig er augljós leið til að skrifa aðferðina sem hér segir:

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

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

Vegna löngunar til að fínstilla smá, og smá ótta við hvað Roslyn muni búa til við að setja saman þennan kóða, geturðu endurskrifað þetta dæmi sem hér segir:

public Task<string> GetById(int id) {

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

Reyndar, besta lausnin í þessu tilfelli væri að fínstilla heita slóðina, þ.e. að fá gildi úr orðabókinni án óþarfa úthlutunar og álags á GC, en í þeim sjaldgæfu tilfellum þegar við þurfum enn að fara í IO fyrir gögn , allt verður áfram plús /mínus á gamla mátann:

public ValueTask<string> GetById(int id) {

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

Lítum nánar á þennan kóða: ef það er gildi í skyndiminni búum við til uppbyggingu, annars verður raunverulega verkefninu vafin inn í þýðingarmikið. Kallkóðann er sama í hvaða slóð þessi kóði var keyrður: ValueTask, frá sjónarhóli C# setningafræðinnar, mun haga sér eins og venjulegt verkefni í þessu tilfelli.

TaskSchedulers: stjórna áætlunum um að hefja verkefni

Næsta API sem ég myndi vilja íhuga er bekkurinn Verkefnaáætlun og afleiður þess. Ég nefndi þegar hér að ofan að TPL hefur getu til að stjórna aðferðum til að dreifa verkefnum yfir þræði. Slíkar aðferðir eru skilgreindar í afkomendum TaskScheduler bekkjarins. Næstum allar stefnur sem þú gætir þurft er að finna á bókasafninu. ParallelExtensions Aukahlutir, þróað af Microsoft, en ekki hluti af .NET, heldur afhent sem Nuget pakki. Við skulum líta stuttlega á nokkrar þeirra:

  • CurrentThreadTaskScheduler — framkvæmir verkefni á núverandi þræði
  • Takmarkaður SamtímisLevelTask ​​Scheduler — takmarkar fjölda verkefna sem eru framkvæmd samtímis með færibreytu N, sem er samþykkt í smiðinum
  • OrderedTaskScheduler — er skilgreint sem LimitedConcurrencyLevelTaskScheduler(1), þannig að verkefni verða framkvæmd í röð.
  • WorkStealingTaskScheduler - verkfæri vinnuþjófnaður nálgun við verkefnadreifingu. Í meginatriðum er það sérstakt ThreadPool. Leysir vandamálið að í .NET ThreadPool er static class, einn fyrir öll forrit, sem þýðir að ofhleðsla hans eða röng notkun í einum hluta forritsins getur leitt til aukaverkana í öðrum. Þar að auki er mjög erfitt að skilja orsök slíkra galla. Það. Það gæti verið þörf á að nota sérstaka WorkStealingTaskSchedulers í hluta forritsins þar sem notkun ThreadPool getur verið árásargjarn og ófyrirsjáanleg.
  • QueuedTaskScheduler — gerir þér kleift að framkvæma verkefni samkvæmt reglum um forgangsröð
  • ThreadPerTask Scheduler — býr til sérstakan þráð fyrir hvert verkefni sem er keyrt á því. Getur verið gagnlegt fyrir verkefni sem taka ófyrirsjáanlega langan tíma að klára.

Það er gott ítarlegt grein um TaskSchedulers á microsoft blogginu.

Fyrir þægilega villuleit á öllu sem tengist Verkefnum er Visual Studio með Verkefnaglugga. Í þessum glugga geturðu séð núverandi stöðu verkefnisins og hoppað á kóðalínuna sem er í gangi.

.NET: Verkfæri til að vinna með fjölþráða og ósamstillingu. 1. hluti

PLinq og Parallel bekknum

Til viðbótar við Tasks og allt sem sagt er um þau eru tvö áhugaverð verkfæri í .NET: PLinq (Linq2Parallel) og Parallel classinn. Sú fyrsta lofar samhliða framkvæmd allra Linq aðgerða á mörgum þráðum. Hægt er að stilla fjölda þráða með WithDegreeOfParallelism framlengingaraðferðinni. Því miður hefur PLinq oftast ekki nægjanlegar upplýsingar um innra hluta gagnagjafans þíns til að veita verulegan hraðaaukningu, á hinn bóginn er kostnaðurinn við að prófa mjög lágur: þú þarft bara að hringja í AsParallel aðferðina áður en keðju Linq aðferða og keyra frammistöðupróf. Þar að auki er hægt að senda viðbótarupplýsingar til PLinq um eðli gagnagjafans þíns með því að nota skiptingarkerfi. Þú getur lesið meira hér и hér.

Parallel static class býður upp á aðferðir til að endurtaka í gegnum Foreach safn samhliða, keyra For lykkju og keyra marga fulltrúa samhliða Invoke. Framkvæmd núverandi þráðar verður stöðvuð þar til útreikningum er lokið. Hægt er að stilla fjölda þráða með því að senda ParallelOptions sem síðustu röksemd. Þú getur líka tilgreint TaskScheduler og CancellationToken með því að nota valkosti.

Niðurstöður

Þegar ég byrjaði að skrifa þessa grein út frá efni skýrslunnar minnar og þeim upplýsingum sem ég safnaði við vinnu mína eftir hana bjóst ég ekki við að það yrði svona mikið af henni. Nú, þegar textaritillinn, sem ég er að skrifa þessa grein í, segir mér ámælisvert að síða 15 sé farin, mun ég draga saman bráðabirgðaniðurstöðurnar. Farið verður yfir önnur brellur, API, sjónræn verkfæri og gildrur í næstu grein.

Ályktanir:

  • Þú þarft að þekkja verkfærin til að vinna með þræði, ósamstillingu og samsvörun til að geta notað auðlindir nútíma tölvur.
  • .NET hefur mörg mismunandi verkfæri í þessum tilgangi
  • Þeir birtust ekki allir í einu, svo þú getur oft fundið eldri, hins vegar eru leiðir til að umbreyta gömlum API án mikillar fyrirhafnar.
  • Vinna með þræði í .NET er táknuð með þræði og þráðahópnum
  • Aðferðirnar Thread.Abort, Thread.Interrupt og Win32 API TerminateThread eru hættulegar og ekki er mælt með notkun þeirra. Í staðinn er betra að nota CancellationToken vélbúnaðinn
  • Flæði er dýrmæt auðlind og framboð þess takmarkað. Forðast ætti aðstæður þar sem þræðir eru uppteknir við að bíða eftir atburðum. Fyrir þetta er þægilegt að nota TaskCompletionSource flokkinn
  • Öflugustu og fullkomnustu .NET verkfærin til að vinna með samhliða og ósamstillingu eru Tasks.
  • C# async/wait rekstraraðilarnir innleiða hugmyndina um bið án lokunar
  • Þú getur stjórnað dreifingu verkefna yfir þræði með því að nota TaskScheduler-afleidda flokka
  • ValueTask uppbyggingin getur verið gagnleg til að hagræða heitum slóðum og minnisumferð
  • Verkefni og þræði gluggar Visual Studio veita mikið af upplýsingum sem eru gagnlegar til að kemba fjölþráða eða ósamstilltan kóða
  • PLinq er flott tól, en það hefur kannski ekki nægar upplýsingar um gagnagjafann þinn, en það er hægt að laga þetta með skiptingarkerfi
  • Til að halda áfram ...

Heimild: www.habr.com

Bæta við athugasemd