.NET: Tools fir mat Multithreading an Asynchronie ze schaffen. Deel 1

Ech publizéieren den ursprénglechen Artikel iwwer Habr, d'Iwwersetzung vun deem ass an der Firma gepost Blog Post.

De Besoin fir eppes asynchron ze maachen, ouni op d'Resultat hei an elo ze waarden, oder grouss Aarbecht tëscht verschiddenen Eenheeten opzedeelen, déi se ausféieren, existéiert virum Advent vu Computeren. Mat hirem Advent gouf dëse Besoin ganz konkret. Elo, am Joer 2019, tippen ech dësen Artikel op engem Laptop mat engem 8-Kär Intel Core Prozessor, op deem méi wéi honnert Prozesser parallel lafen, an nach méi Threads. An der Géigend gëtt et e liicht schaarf Telefon, dee virun e puer Joer kaaft gouf, et huet en 8-Core Prozessor u Bord. Thematesch Ressourcen si voll mat Artikelen a Videoen wou hir Autoren dëst Joer Flaggschëff Smartphones bewonneren déi 16-Kär Prozessoren hunn. MS Azure bitt eng virtuell Maschinn mat engem 20 Kär Prozessor an 128 TB RAM fir manner wéi $ 2 / Stonn. Leider ass et onméiglech de Maximum ze extrahieren an dës Kraaft ze profitéieren ouni d'Interaktioun vu Threads ze verwalten.

Terminologie

Prozess - OS Objet, isoléiert Adressraum, enthält Threads.
Fuedem - en OS Objet, déi klengst Eenheet vun der Ausféierung, Deel vun engem Prozess, Threads deelen Erënnerung an aner Ressourcen ënner sech bannent engem Prozess.
Multitasking - OS Eegeschafte, d'Fäegkeet fir verschidde Prozesser gläichzäiteg ze lafen
Multi-Kär - e Besëtz vum Prozessor, d'Fäegkeet fir verschidde Käre fir d'Dateveraarbechtung ze benotzen
Multiprocessing - e Besëtz vun engem Computer, d'Fäegkeet fir gläichzäiteg mat verschiddene Prozessoren kierperlech ze schaffen
Multithreading - e Besëtz vun engem Prozess, d'Fäegkeet fir Datenveraarbechtung tëscht verschiddene Threads ze verdeelen.
Parallelismus - verschidden Aktiounen kierperlech gläichzäiteg pro Zäitunitéit ausféieren
Asynchronie - Ausféierung vun enger Operatioun ouni op d'Fäerdegstellung vun dëser Veraarbechtung ze waarden, kann d'Resultat vun der Ausféierung spéider veraarbecht ginn.

Metapher

Net all Definitioune si gutt an e puer brauchen zousätzlech Erklärung, also wäert ech eng Metapher iwwer Kachen Frühstück un déi formell agefouert Terminologie addéieren. Kachen Frühstück an dëser Metapher ass e Prozess.

Beim Virbereedung vum Frühstück moies hunn ech (cpu) Ech kommen an d'Kichen (Computer). Ech hunn 2 Hänn (Placken). Et ginn eng Rei vun Apparater an der Kichen (IO): Uewen, Kettel, Toaster, Frigo. Ech schalten de Gas un, setzen eng Bratpfanne drop a schëdden Ueleg an et ouni ze waarden bis et ophëtzt (asynchronously, Non-Blocking-IO-Waart), Ech huelen d'Eeër aus dem Frigo a briechen se an eng Teller, schloen se dann mat enger Hand (Thema #1), an zweet (Thema #2) hält de Plack (Shared Ressource). Elo wéilt ech de Kettel opmaachen, awer ech hunn net genuch Hänn (Thread Hunger) Wärend dëser Zäit erhëtzt d'Bratpfanne (Veraarbechtung vum Resultat) an deem ech schëdden wat ech geschloen hunn. Ech erreechen de Kettel a schalten en un a kucken domm wéi d'Waasser dran kachen (Blocking-IO-Waart), obwuel hien während dëser Zäit d'Teller wäschen hätt, wou hien den Omelett geschloen huet.

Ech hunn eng Omelett mat nëmmen 2 Hänn gekacht, an ech hunn net méi, awer gläichzäiteg, am Moment vum Pechpabeier, sinn 3 Operatiounen gläichzäiteg stattfonnt: d'Omelett schloen, den Teller halen, d'Bratpfanne erhëtzen D'CPU ass dee schnellsten Deel vum Computer, IO ass dat wat meeschtens alles verlangsamt ass, also ass eng effektiv Léisung d'CPU mat eppes ze besetzen wärend Dir Daten vun IO kritt.

Weider mat der Metapher:

  • Wann ech am Prozess vun der Preparatioun vun engem Omelet probéieren och Kleeder ze änneren, dëst wier e Beispill vu Multitasking. Eng wichteg Nuance: Computeren sinn vill besser an dësem wéi Leit.
  • Eng Kichen mat verschiddene Kichecheffe, zum Beispill an engem Restaurant - e Multi-Core Computer.
  • Vill Restauranten an engem Liewensmëttel Geriicht an engem Akafszenter - Daten Zentrum

.NET Tools

.NET ass gutt fir mat Threads ze schaffen, wéi mat villen anere Saachen. Mat all neier Versioun gëtt et ëmmer méi nei Tools fir mat hinnen ze schaffen, nei Schichten vun der Abstraktioun iwwer OS Threads. Wann Dir mat der Konstruktioun vun Abstraktiounen schafft, benotze Framework Entwéckler eng Approche déi d'Méiglechkeet léisst, wann Dir eng High-Level Abstraktioun benotzt, een oder méi Niveauen ënnen erofgoen. Meeschtens ass dëst net néideg, tatsächlech mécht et d'Dier op fir Iech selwer an de Fouss mat engem Gewier ze schéissen, awer heiansdo, a rare Fäll, kann et deen eenzege Wee sinn fir e Problem ze léisen deen net um aktuellen Abstraktiounsniveau geléist gëtt .

Mat Tools mengen ech souwuel Applikatiounsprogramméierungsinterfaces (APIs), déi vum Kader geliwwert ginn an Drëtt-Partei Packagen, souwéi ganz Softwareléisungen, déi d'Sich no Probleemer am Zesummenhang mat Multi-threaded Code vereinfachen.

Fänkt e Fuedem un

D'Thread Klass ass déi meescht Basis Klass am .NET fir mat Threads ze schaffen. De Konstruktor akzeptéiert ee vun zwee Delegéierten:

  • ThreadStart - Keng Parameteren
  • ParametrizedThreadStart - mat engem Parameter vum Typ Objet.

Den Delegéierte gëtt am nei geschafene Fuedem ausgefouert nodeems d'Startmethod geruff gouf Wann en Delegéierte vum Typ ParametrizedThreadStart un de Konstruktor weidergeleet gouf, da muss en Objet un d'Startmethod weiderginn. Dëse Mechanismus ass néideg fir all lokal Informatioun op de Stream ze transferéieren. Et ass derwäert ze bemierken datt d'Erstelle vun engem Fuedem eng deier Operatioun ass, an de Fuedem selwer ass e schwéieren Objet, op d'mannst well et 1MB Erënnerung um Stack verdeelt an Interaktioun mat der OS API erfuerdert.

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

D'ThreadPool Klass representéiert d'Konzept vun engem Pool. Am .NET ass de Fuedempool e Stéck Ingenieur, an d'Entwéckler bei Microsoft hu vill Effort gemaach fir sécher ze stellen datt et an enger grousser Villfalt vun Szenarien optimal funktionnéiert.

Allgemeng Konzept:

Vun deem Moment un d'Applikatioun ufänkt, erstellt e puer Threads an der Reserve am Hannergrond a bitt d'Fäegkeet se fir ze benotzen. Wann Threads dacks a grousser Zuel benotzt ginn, erweidert de Pool fir de Bedierfnesser vum Uruffer ze treffen. Wann et keng gratis Threads am Pool zu der richteger Zäit sinn, wäert et entweder waarden op ee vun de Threads zréck, oder en neien erstellen. Et folgt datt de Fuedempool super ass fir e puer kuerzfristeg Aktiounen a schlecht gëeegent fir Operatiounen déi als Servicer während der ganzer Operatioun vun der Applikatioun lafen.

Fir e Fuedem aus dem Pool ze benotzen, gëtt et eng QueueUserWorkItem Method, déi en Delegéierte vum Typ WaitCallback akzeptéiert, deen déiselwecht Ënnerschrëft huet wéi ParametrizedThreadStart, an de Parameter, deen dohinner geliwwert gëtt, mécht déiselwecht Funktioun.

ThreadPool.QueueUserWorkItem(...);

Déi manner bekannt Thread Pool Method RegisterWaitForSingleObject gëtt benotzt fir net blockéierend IO Operatiounen ze organiséieren. Den Delegéierten, deen op dës Methode passéiert ass, gëtt genannt wann de WaitHandle an d'Method iwwerginn ass "Released".

ThreadPool.RegisterWaitForSingleObject(...)

.NET huet e thread Timer an et ënnerscheet sech vun WinForms / WPF Timer an datt seng Handler op engem Fuedem aus dem Pool geholl gëtt.

System.Threading.Timer

Et gëtt och eng zimlech exotesch Manéier fir en Delegéierte fir d'Ausféierung an e Fuedem aus dem Pool ze schécken - d'BeginInvoke Method.

DelegateInstance.BeginInvoke

Ech wëll kuerz op d'Funktioun ophalen, op déi vill vun den uewe genannte Methoden genannt kënne ginn - CreateThread vun Kernel32.dll Win32 API. Et gëtt e Wee, dank dem Mechanismus vun externe Methoden, dës Funktioun ze nennen. Ech hunn esou en Uruff nëmmen eemol an engem schreckleche Beispill vum Legacy Code gesinn, an d'Motivatioun vum Auteur, dee genau dat gemaach huet, bleift fir mech nach ëmmer e Geheimnis.

Kernel32.dll CreateThread

Kuckt an Debugging Threads

Threads erstallt vun Iech, all Drëtt-Partei Komponenten, an den .NET Pool kënnen an der Threads Fënster vum Visual Studio gekuckt ginn. Dës Fënster wäert nëmmen thread Informatioun weisen wann d'Applikatioun ënner Debug an am Break Modus ass. Hei kënnt Dir bequem d'Stack Nimm a Prioritéite vun all thread gesinn, an Debugging op e spezifesche Fuedem wiesselen. Mat der Prioritéit Eegeschafte vun der Thread Klass kënnt Dir d'Prioritéit vun engem Fuedem setzen, deen den OC an CLR als Empfehlung gesinn wann d'Prozessor Zäit tëscht Threads opgedeelt gëtt.

.NET: Tools fir mat Multithreading an Asynchronie ze schaffen. Deel 1

Aufgab Parallel Bibliothéik

Aufgab Parallel Bibliothéik (TPL) gouf an .NET 4.0 agefouert. Elo ass et de Standard an den Haaptinstrument fir mat Asynchronie ze schaffen. All Code deen eng méi al Approche benotzt gëtt als Legacy ugesinn. D'Basis Eenheet vun TPL ass d'Task Klass vum System.Threading.Tasks Nummraum. Eng Aufgab ass eng Abstraktioun iwwer e Fuedem. Mat der neier Versioun vun der C # Sprooch hu mir en elegante Wee fir mat Tasks ze schaffen - async / wait Operators. Dës Konzepter hunn et méiglech gemaach asynchrone Code ze schreiwen wéi wann et einfach a synchron wier, dëst huet et méiglech gemaach souguer fir Leit mat wéineg Verständnis vun der interner Aarbecht vu Threads fir Uwendungen ze schreiwen déi se benotzen, Uwendungen déi net afréieren wann se laang Operatiounen ausféieren. Async / await benotzen ass en Thema fir een oder souguer e puer Artikelen, awer ech probéieren d'Geschwindegkeet an e puer Sätz ze kréien:

  • async ass e Modifikateur vun enger Method déi Task oder Void zréckkënnt
  • a waart ass en net blockéierende Task Waarden Bedreiwer.

Nach eng Kéier: de Wait Operator, am allgemenge Fall (et ginn Ausnahmen), wäert den aktuelle Fuedem vun der Ausféierung weider verëffentlechen, a wann d'Task seng Ausféierung fäerdeg ass, an de Fuedem (tatsächlech wier et méi korrekt de Kontext ze soen , awer méi iwwer dat méi spéit) wäert d'Method weider ausféieren. Bannen .NET gëtt dëse Mechanismus op déiselwecht Manéier ëmgesat wéi d'Ausbezuelung zréck, wann d'schrëftlech Method an eng ganz Klass ëmgeet, déi eng Staatsmaschinn ass a kann a separat Stécker ofhängeg vun dëse Staaten ausgefouert ginn. Jiddereen interesséiert kann all einfache Code schreiwen mat asynс / wait, kompiléieren a kucken d'Versammlung mat JetBrains dotPeek mat Compiler Generated Code aktivéiert.

Loosst eis d'Optiounen kucken fir Task ze starten an ze benotzen. Am Code Beispill hei drënner erstellen mir eng nei Aufgab déi näischt nëtzlech mécht (Thread.Schlof (10000)), awer am richtege Liewen sollt dëst eng komplex CPU-intensiv Aarbecht sinn.

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
}

Eng Task gëtt mat enger Zuel vun Optiounen erstallt:

  • LongRunning ass en Hiweis datt d'Aufgab net séier ofgeschloss gëtt, dat heescht datt et derwäert ka sinn ze berücksichtegen net e Fuedem aus dem Pool ze huelen, awer en separaten fir dës Aufgab ze kreéieren fir anerer net ze schueden.
  • AttachedToParent - Aufgaben kënnen an enger Hierarchie arrangéiert ginn. Wann dës Optioun benotzt gouf, da kann d'Task an engem Zoustand sinn, wou se selwer fäerdeg ass a waart op d'Ausféierung vu senge Kanner.
  • PreferFairness - heescht datt et besser wier Aufgaben auszeféieren, déi fir d'Ausféierung geschéckt ginn, ier déi spéider geschéckt ginn. Awer dëst ass just eng Empfehlung an d'Resultater sinn net garantéiert.

Den zweete Parameter, deen un d'Method weidergeleet gëtt, ass CancellationToken. Fir d'Annulatioun vun enger Operatioun korrekt ze handhaben nodeems se ugefaang huet, muss de Code deen ausgefouert gëtt mat Schecken fir den CancellationToken Staat gefëllt ginn. Wann et keng Kontrolle gëtt, da kann d'Annuléierungsmethod, déi um CancellationTokenSource-Objet genannt gëtt, d'Ausféierung vun der Task nëmmen stoppen ier se ufänkt.

De leschte Parameter ass e Schedulerobjekt vum Typ TaskScheduler. Dës Klass a seng Nokommen sinn entwéckelt fir Strategien ze kontrolléieren fir Aufgaben iwwer Threads ze verdeelen, d'Task gëtt op engem zoufällege Fuedem aus dem Pool ausgefouert.

Den Erwaart Bedreiwer gëtt op déi erstallt Task ugewannt, dat heescht datt de Code, deen duerno geschriwwe gëtt, wann et een ass, am selwechte Kontext ausgefouert gëtt (dacks heescht dat op deemselwechte Fuedem) wéi de Code virdru waart.

D'Method ass als async ongëlteg markéiert, dat heescht datt et den Erwaardungsoperateur ka benotzen, awer den Uruffcode kann net op d'Ausféierung waarden. Wann esou eng Feature néideg ass, da muss d'Method Task zréckginn. Methoden markéiert async ongëlteg sinn zimlech heefeg: als Regel, dës sinn Event Handler oder aner Methoden, datt op d'Feier Aarbecht a vergiess Prinzip. Wann Dir braucht net nëmmen d'Méiglechkeet ze waarden bis d'Enn vun der Ausféierung, awer och d'Resultat zréckzekommen, da musst Dir Task benotzen.

Op der Task déi d'StartNew Method zréckginn, wéi och op all aner, kënnt Dir d'ConfigureAwait Method mat dem falschen Parameter nennen, da wäert d'Ausféierung no der Erwaardung weider net op de erfaasste Kontext, mee op engem arbiträren. Dëst sollt ëmmer gemaach ginn wann den Ausféierungskontext net wichteg ass fir de Code no der Erwaardung. Dëst ass och eng Empfehlung vu MS wann Dir Code schreift deen an enger Bibliothéik verpackt gëtt.

Loosst eis e bësse méi iwwer wéi Dir kënnt op d'Réalisatioun vun enger Aufgab waarden. Drënner ass e Beispill vu Code, mat Kommentaren iwwer wann d'Erwaardung bedingt gutt gemaach gëtt a wann et bedingt schlecht gemaach gëtt.

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
}

Am éischte Beispill waarden mir op d'Task fir den Uruff thread ze blockéieren, wäerte mir zréck op d'Veraarbechtung vum Resultat eréischt wann et bis dohinner ass, gëtt de Ruff thread op seng eegen Apparater gelooss;

An der zweeter Optioun blockéiere mir den Uruff thread bis d'Resultat vun der Method berechent gëtt. Dëst ass schlecht net nëmme well mir e Fuedem, sou eng wäertvoll Ressource vum Programm, mat einfacher Idleness besat hunn, awer och well wann de Code vun der Method déi mir nennen enthält waart, an de Synchroniséierungskontext erfuerdert zréck an den Uruff thread no waart, da kréie mer en Deadlock : De Calling thread waart op d'Resultat vun der asynchroner Method fir berechent ze ginn, déi asynchron Method probéiert vergeblech seng Ausféierung am Call thread weiderzeféieren.

En aneren Nodeel vun dëser Approche ass komplizéiert Fehlerhandhabung. D'Tatsaach ass datt Feeler am asynchrone Code beim Asynchroniséierung benotzt / waarden ganz einfach ze handhaben - si behuelen d'selwecht wéi wann de Code synchron wier. Wärend wa mir synchronen Waardeexorcismus op eng Task applizéieren, gëtt déi ursprénglech Ausnam an eng AggregateException, d.h. Fir d'Ausnam ze handhaben, musst Dir den InnerException Typ ënnersichen an eng If Kette selwer an engem Fangblock schreiwen oder de Fang benotze wann Dir konstruktéiert, amplaz vun der Kette vu Fangblocken déi méi vertraut ass an der C # Welt.

Déi drëtt a lescht Beispiller sinn och schlecht aus dem selwechte Grond markéiert an enthalen all déiselwecht Problemer.

D'WhenAny an WhenAll Methoden sinn extrem praktesch fir op eng Grupp vun Aufgaben ze waarden, si wéckelen eng Grupp vun Aufgaben an een, deen entweder brennt wann eng Task aus der Grupp fir d'éischt ausgeléist gëtt, oder wann se all hir Ausféierung ofgeschloss hunn.

Stoppen thread

Aus verschiddene Grënn kann et néideg sinn de Flux ze stoppen nodeems se ugefaang huet. Et ginn eng Rei vu Weeër dëst ze maachen. D'Thread Klass huet zwou entspriechend benannt Methoden: Ofbriechen и Ënnerbriechen. Déi éischt ass net recommandéiert fir ze benotzen, well nodeems se zu all zoufälleg Moment ruffen, während der Veraarbechtung vun all Instruktioun, gëtt eng Ausnam geworf ThreadAbortedException. Dir erwaart net datt sou eng Ausnam geheit gëtt wann Dir eng ganz Zuel Variabel eropgeet, richteg? A wann Dir dës Method benotzt, ass dëst eng ganz real Situatioun. Wann Dir musst verhënneren datt de CLR esou eng Ausnahm an enger bestëmmter Sektioun vum Code generéiert, kënnt Dir et an Uruff wéckelen Thread.BeginCriticalRegion, Thread.EndCriticalRegion. All Code, deen an engem endlech Block geschriwwe gëtt, ass an esou Uriff gewéckelt. Aus dësem Grond, an der Tiefe vum Kadercode kënnt Dir Blöcke mat engem eidele Versuch fannen, awer net endlech eidel. Microsoft decouragéiert dës Method sou vill datt se se net am .net Kär enthalen hunn.

D'Interrupt Method funktionnéiert méi prévisibel. Et kann de Fuedem mat enger Ausnam ënnerbriechen ThreadInterruptedException nëmmen an deene Momenter wou de Fuedem an engem waarden Zoustand ass. Et geet an dësem Zoustand iwwerdeems op WaitHandle gewaart, Spär, oder nodeems se Thread.Sleep Opruff.

Béid Optiounen uewen beschriwwen sinn schlecht well se onberechenbaren sinn. D'Léisung ass eng Struktur ze benotzen CancellationToken an Klass CancellationTokenSource. De Punkt ass dëst: eng Instanz vun der Klass CancellationTokenSource gëtt erstallt an nëmmen deen deen se besëtzt kann d'Operatioun stoppen andeems Dir d'Method rufft annuléieren. Nëmmen den CancellationToken gëtt un d'Operatioun selwer weiderginn. CancellationToken Besëtzer kënnen d'Operatioun net selwer annuléieren, awer kënnen nëmme kontrolléieren ob d'Operatioun annuléiert gouf. Et gëtt eng boolesch Immobilie fir dëst IsCancellationRequested a Method ThrowIfCancelRequested. Déi lescht wäert eng Ausnahm werfen TaskCancelledException wann d'Cancel Method op der CancellationToken Instanz genannt gouf, déi parrotéiert gëtt. An dëst ass d'Method déi ech recommandéieren ze benotzen. Dëst ass eng Verbesserung iwwer déi vireg Optiounen andeems Dir voll Kontroll kritt iwwer op wéi engem Punkt eng Ausnahmsoperatioun ofgebrach ka ginn.

Déi brutalst Optioun fir e Fuedem ze stoppen ass d'Win32 API TerminateThread Funktioun ze nennen. D'Behuele vum CLR nom Uruff vun dëser Funktioun kann onberechenbar sinn. Op MSDN gëtt déi folgend iwwer dës Funktioun geschriwwen: "TerminateThread ass eng geféierlech Funktioun déi nëmmen an den extremsten Fäll benotzt soll ginn. "

Konvertéieren Legacy API op Task Based mat der FromAsync Method

Wann Dir Gléck genuch hutt un engem Projet ze schaffen, deen ugefaang gouf nodeems d'Tasks agefouert goufen an opgehalen hunn roueg Horror fir déi meescht Entwéckler ze verursaachen, da musst Dir net mat vill alen APIen këmmeren, souwuel Drëtt Parteien an deenen Äert Team huet an der Vergaangenheet gefoltert. Glécklecherweis huet d'.NET Framework Team sech ëm eis gekëmmert, obwuel vläicht d'Zil war eis selwer ze këmmeren. Sief dat, .NET huet eng Rei vun Tools fir schmerzhafte Konvertéierung vun Coden, déi an alen asynchronen Programméierungs Approche geschriwwe sinn, op déi nei. Ee vun hinnen ass d'FromAsync Method vun TaskFactory. Am Code Beispill hei ënnen wéckelen ech déi al Async Methoden vun der WebRequest Klass an enger Task mat dëser Method.

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

Dëst ass just e Beispill an Dir sidd onwahrscheinlech dëst mat agebauten Typen ze maachen, awer all alen Projet ass einfach mat BeginDoSomething Methoden déi IAsyncResult an EndDoSomething Methoden zréckginn déi et kréien.

Konvertéiert legacy API op Task Based mat TaskCompletionSource Klass

En anert wichtegt Instrument fir ze berücksichtegen ass d'Klass TaskCompletionSource. Wat d'Funktiounen, Zweck an Operatiounsprinzip ugeet, kann et e bëssen un d'RegisterWaitForSingleObject Method vun der ThreadPool Klass erënneren, iwwer déi ech uewen geschriwwen hunn. Mat dëser Klass kënnt Dir einfach a bequem al asynchron APIen an Tasks wéckelen.

Dir wäert soen datt ech schonn iwwer d'FromAsync Method vun der TaskFactory Klass geschwat hunn fir dës Zwecker geduecht. Hei musse mir déi ganz Geschicht vun der Entwécklung vun asynchrone Modeller am .net erënneren, déi Microsoft an de leschte 15 Joer offréiert huet: virum Task-Based Asynchronous Pattern (TAP) gouf et d'Asynchronous Programming Pattern (APP), déi war iwwer Methoden BeginDoSomething zréck IAsyncResult a Methoden EnnDoSomething dat akzeptéiert a fir d'Ierfschaft vun dëse Joeren ass d'FromAsync Method just perfekt, awer mat der Zäit gouf se duerch den Event Based Asynchronous Pattern ersat (AN AP), déi ugeholl datt en Event opgeworf gëtt wann déi asynchron Operatioun fäerdeg ass.

TaskCompletionSource ass perfekt fir Aufgaben an legacy APIs ronderëm den Eventmodell ze wéckelen. D'Essenz vu senger Aarbecht ass wéi follegt: en Objet vun dëser Klass huet eng ëffentlech Eegeschafte vum Typ Task, den Zoustand vun deem kann duerch d'SetResult, SetException, etc. Methode vun der TaskCompletionSource Klass kontrolléiert ginn. Op Plazen wou den Erwaart Bedreiwer op dës Task applizéiert gouf, gëtt se ausgefouert oder versoen mat enger Ausnam ofhängeg vun der Method déi op der TaskCompletionSource applizéiert gëtt. Wann et nach ëmmer net kloer ass, loosst eis dëst Code Beispill kucken, wou e puer al EAP API an enger Task gewéckelt ass mat enger TaskCompletionSource: wann d'Evenement brennt, gëtt d'Task an de Fäerdeg Zoustand transferéiert, an d'Method déi den Erwaart Bedreiwer ugewannt huet fir dës Aufgab wäert hir Ausféierung erëmfannen nodeems se den Objet kritt hunn Resultat.

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 Tipps & Tricks

Wrapping al APIen ass net alles wat mat TaskCompletionSource gemaach ka ginn. D'Benotzung vun dëser Klass mécht eng interessant Méiglechkeet op fir verschidde APIen op Aufgaben ze designen déi net Threads besetzen. An de Stroum, wéi mir eis erënneren, ass eng deier Ressource an hir Zuel ass limitéiert (haaptsächlech duerch d'Quantitéit vum RAM). Dës Begrenzung kann einfach erreecht ginn andeems Dir zum Beispill eng gelueden Webapplikatioun mat komplexer Geschäftslogik entwéckelt. Loosst eis d'Méiglechkeeten betruechten, vun deenen ech schwätzen, wann Dir esou en Trick wéi Long-Polling ëmsetzt.

Kuerz gesot, d'Essenz vum Trick ass dëst: Dir musst Informatioun vun der API iwwer e puer Eventer kréien, déi op senger Säit geschéien, während d'API, aus irgendege Grënn, d'Evenement net mellen kann, awer nëmmen de Staat zréckginn. E Beispill vun dësen sinn all APIen uewen op HTTP gebaut virun der Zäit vum WebSocket oder wann et aus irgendege Grënn onméiglech war dës Technologie ze benotzen. De Client kann den HTTP-Server froen. Den HTTP-Server kann net selwer Kommunikatioun mam Client initiéieren. Eng einfach Léisung ass de Server mat engem Timer ze pollen, awer dëst schaaft eng zousätzlech Belaaschtung op de Server an eng zousätzlech Verzögerung am Duerchschnëtt TimerInterval / 2. Fir dëst ëmzegoen, gouf en Trick mam Numm Long Polling erfonnt, deen d'Äntwert op d'Verzögerung vun de Server bis den Timeout ofleeft oder en Event geschitt. Wann en Event geschitt, gëtt et veraarbecht wann net, gëtt d'Ufro erëm geschéckt.

while(!eventOccures && !timeoutExceeded)  {

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

Awer esou eng Léisung wäert schrecklech beweisen, soubal d'Zuel vun de Clienten, déi op d'Evenement waarden, eropgeet, well ... All esou Client besetzt e ganze Fuedem fir op en Event ze waarden. Jo, a mir kréien eng zousätzlech 1ms Verspéidung wann d'Evenement ausgeléist gëtt, meeschtens ass dëst net bedeitend, awer firwat d'Software méi schlëmm maachen wéi et ka sinn? Wa mir Thread.Sleep (1) ewechhuelen, da wäerte mir ëmsoss ee Prozessorkär 100% Idle lueden, an engem onnëtzten Zyklus rotéieren. Mat TaskCompletionSource kënnt Dir dëse Code einfach nei maachen an all d'Problemer uewe identifizéiert léisen:

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

Dëse Code ass net Produktioun-prett, mee just eng Demo. Fir et an echte Fäll ze benotzen, musst Dir och op d'mannst d'Situatioun handhaben wann e Message op eng Zäit kënnt wou keen et erwaart: an dësem Fall sollt d'AsseptMessageAsync Method eng scho fäerdeg Aufgab zréckginn. Wann dëst deen heefegste Fall ass, da kënnt Dir drun denken ValueTask ze benotzen.

Wa mir eng Ufro fir e Message kréien, erstellen a placéiere mir eng TaskCompletionSource am Wierderbuch, a waarden dann op wat fir d'éischt geschitt: de spezifizéierte Zäitintervall leeft aus oder e Message gëtt kritt.

ValueTask: firwat a wéi

D'Async / Waart Opérateuren, wéi de Rendement Retour Bedreiwer, generéieren eng Staatsmaschinn aus der Method, an dëst ass d'Schafe vun engem neien Objet, wat bal ëmmer net wichteg ass, awer an rare Fäll kann et e Problem kreéieren. Dëse Fall kann eng Method sinn, déi wierklech dacks genannt gëtt, mir schwätzen iwwer Zénger an Honnerte vun Dausende vun Uriff pro Sekonn. Wann esou eng Method esou geschriwwe gëtt datt se an de meeschte Fäll e Resultat zréckginn, déi all Erwaardungsmethoden ëmgoen, da bitt .NET en Tool fir dëst ze optimiséieren - d'ValueTask Struktur. Fir et kloer ze maachen, kucke mer e Beispill vu senger Notzung: et gëtt e Cache op dee mir ganz dacks goen. Et enthält e puer Wäerter an da gi mir se einfach zréck, wann net, da gi mir op e luesen IO fir se ze kréien. Ech wëll dat lescht asynchron maachen, dat heescht datt déi ganz Method asynchron ass. Also ass de offensichtleche Wee fir d'Method ze schreiwen ass wéi follegt:

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

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

Wéinst dem Wonsch e bëssen ze optimiséieren, an eng liicht Angscht vun deem wat Roslyn generéiert wann Dir dëse Code kompiléiert, kënnt Dir dëst Beispill wéi follegt ëmschreiwen:

public Task<string> GetById(int id) {

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

Tatsächlech wier déi optimal Léisung an dësem Fall den Hot-Pad ze optimiséieren, nämlech e Wäert aus dem Wierderbuch ze kréien ouni onnéideg Allokatiounen a Belaaschtung op de GC, wärend an deene rare Fäll wou mir nach ëmmer op IO fir Daten musse goen , alles bleift e Plus / Minus op déi al Manéier:

public ValueTask<string> GetById(int id) {

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

Loosst eis dëst Stéck Code méi no kucken: wann et e Wäert am Cache gëtt, kreéiere mir eng Struktur, soss gëtt déi richteg Aufgab an eng sënnvoll gewéckelt. Den Uruffcode ass egal op wéi engem Wee dëse Code ausgefouert gouf: ValueTask, aus enger C# Syntax Siicht, wäert sech an dësem Fall d'selwecht behuelen wéi eng normal Aufgab.

TaskSchedulers: Gestioun vun Taskstartstrategien

Déi nächst API déi ech wëlle berécksiichtegen ass d'Klass TaskScheduler a seng Derivate. Ech hunn schonn uewen ernimmt datt TPL d'Fäegkeet huet Strategien ze managen fir Aufgaben iwwer Threads ze verdeelen. Esou Strategien sinn an den Nokommen vun der TaskScheduler Klass definéiert. Bal all Strategie déi Dir braucht kann an der Bibliothéik fonnt ginn. ParallelExtensionsExtras, entwéckelt vu Microsoft, awer net Deel vun .NET, awer als Nuget Package geliwwert. Loosst eis kuerz e puer vun hinnen kucken:

  • CurrentThreadTaskScheduler - féiert Aufgaben am aktuellen Thread aus
  • Limitéiert ConcurrencyLevelTaskScheduler - limitéiert d'Zuel vun Aufgaben, déi gläichzäiteg vum Parameter N ausgefouert ginn, deen am Konstruktor akzeptéiert gëtt
  • OrderedTaskScheduler - ass definéiert als LimitedConcurrencyLevelTaskScheduler (1), sou datt d'Aufgaben sequenziell ausgefouert ginn.
  • WorkStealingTaskScheduler - Ëmsetzung Aarbecht-klauen Approche fir Aufgab Verdeelung. Wesentlech ass et eng separat ThreadPool. Léist de Problem datt am .NET ThreadPool eng statesch Klass ass, eng fir all Uwendungen, dat heescht datt seng Iwwerlaaschtung oder falsch Benotzung an engem Deel vum Programm zu Nebenwirkungen an engem aneren kann féieren. Ausserdeem ass et extrem schwéier d'Ursaach vun esou Mängel ze verstoen. Dat. Et kann e Besoin sinn fir separat WorkStealingTaskSchedulers an Deeler vum Programm ze benotzen, wou d'Benotzung vun ThreadPool aggressiv an onberechenbar ass.
  • QueuedTaskScheduler - erlaabt Iech Aufgaben no Prioritéitsschlaangregelen auszeféieren
  • ThreadPerTaskScheduler - erstellt e separate Fuedem fir all Aufgab déi dorop ausgefouert gëtt. Kann nëtzlech sinn fir Aufgaben déi onberechenbar laang Zäit huelen fir ze kompletéieren.

Et gëtt eng gutt detailléiert en Artikel iwwer TaskSchedulers um Microsoft Blog.

Fir praktesch Debugging vun allem wat mat Aufgaben Zesummenhang ass, huet Visual Studio eng Aufgabenfenster. An dëser Fënster kënnt Dir den aktuellen Zoustand vun der Aufgab gesinn a sprangen op déi aktuell ausféierend Codelinn.

.NET: Tools fir mat Multithreading an Asynchronie ze schaffen. Deel 1

PLinq an der Parallel Klass

Zousätzlech zu Aufgaben an alles wat iwwer si gesot gëtt, ginn et zwee méi interessant Tools am .NET: PLinq (Linq2Parallel) an d'Parallel Klass. Déi éischt versprécht parallel Ausféierung vun all Linq Operatiounen op verschidde Threads. D'Zuel vun de Threads kann mat der WithDegreeOfParallelism Extensiounsmethod konfiguréiert ginn. Leider huet PLinq meeschtens a sengem Standardmodus net genuch Informatioun iwwer d'Intern vun Ärer Datequell fir e wesentleche Geschwindegkeetsgewënn ze bidden, op der anerer Säit sinn d'Käschte fir ze probéieren ganz niddereg: Dir musst just d'AsParallel Method ruffen ier d'Kette vu Linq Methoden a Leeschtung Tester lafen. Ausserdeem ass et méiglech zousätzlech Informatioun un PLinq iwwer d'Natur vun Ärer Datequell duerch de Partitionsmechanismus weiderzeginn. Dir kënnt méi liesen hei и hei.

D'Parallel statesch Klass bitt Methoden fir duerch eng Foreach Sammlung parallel ze iteréieren, e For Loop auszeféieren, a multiple Delegéierten parallel Invoke auszeféieren. D'Ausféierung vum aktuelle Fuedem gëtt gestoppt bis d'Berechnungen ofgeschloss sinn. D'Zuel vun de thread kann konfiguréiert ginn andeems ParallelOptions als lescht Argument passéiert. Dir kënnt och TaskScheduler an CancellationToken mat Optiounen uginn.

Conclusiounen

Wéi ech ugefaang hunn dësen Artikel ze schreiwen op Basis vun de Materialien vu mengem Bericht an d'Informatiounen, déi ech während menger Aarbecht duerno gesammelt hunn, hunn ech net erwaart datt et sou vill dovu géif ginn. Elo, wann den Texteditor an deem ech dësen Artikel tippen mir reprochéiert seet datt d'Säit 15 fort ass, wäert ech d'Tëscheresultater resuméieren. Aner Tricken, APIen, visuell Tools a Fallen ginn am nächsten Artikel ofgedeckt.

Konklusiounen:

  • Dir musst d'Tools kennen fir mat Threads, Asynchronie a Parallelismus ze schaffen fir d'Ressourcen vun modernen PCs ze benotzen.
  • .NET huet vill verschidden Tools fir dës Zwecker
  • Net all vun hinnen sinn op eemol opgetaucht, sou datt Dir dacks Legacy fannt, awer et gi Weeër fir al APIen ouni vill Effort ze konvertéieren.
  • Schafft mat Threads am .NET gëtt duerch d'Thread an ThreadPool Klassen vertrueden
  • D'Thread.Abort, Thread.Interrupt, a Win32 API TerminateThread Methode si geféierlech a sinn net recommandéiert fir ze benotzen. Amplaz ass et besser de CancellationToken Mechanismus ze benotzen
  • Flow ass eng wäertvoll Ressource a seng Versuergung ass limitéiert. Situatiounen wou Threads beschäftegt sinn op Eventer ze waarden, sollten vermeit ginn. Fir dëst ass et bequem d'TaskCompletionSource Klass ze benotzen
  • Déi mächtegst a fortgeschratt .NET Tools fir mat Parallelismus an Asynchronie ze schaffen sinn Aufgaben.
  • D'c # async / wait Bedreiwer implementéieren d'Konzept vun net blockéierend Waart
  • Dir kënnt d'Verdeelung vun Aufgaben iwwer Threads kontrolléieren mat TaskScheduler-ofgeleet Klassen
  • D'ValueTask Struktur kann nëtzlech sinn fir Hot-Weeër an Erënnerungsverkéier ze optimiséieren
  • Visual Studio's Tasks and Threads Fënstere bidden vill Informatioun nëtzlech fir Multi-threaded oder asynchrone Code Debugging
  • PLinq ass e coolt Tool, awer et kann net genuch Informatioun iwwer Är Datequell hunn, awer dëst kann mat der Partitionéierungsmechanismus fixéiert ginn
  • Fir weidergitt ...

Source: will.com

Setzt e Commentaire