\


 Monday, 04 May 2020
C++ Task Parallel Library (TPL)

Příspěvek nejen pro ty, kteří se od března do června nedočkají kvůli COVIDu C++ kurzu. Kdyby se někdo nudil, nebo mu lezly na mozek ty neustále se opakující zprávy o Babišovi, rouškách a další variace na mem "všichni tady chcípnem, když ne na COVID, tak na sucho", dovolím si upozornit na jednu poměrně čerstvou (neoptimalizovaná alfa) asynchronní švestičku ze své zahrádky. Švestičky i optimisticky naznačují roční období, kdy se snad uvidíme, jestliže macecha příroda nereleasne implementaci zmutované specifikace COVID++.

RStein.AsyncCpp používající coroutine z C++ 20 je knihovna, ve které se rychle zorientuje každý, kdo zná Task Parallel Library (.NET, C#).

V knihovně najdete nejen:

Task (tedy něco jako std:future) - tásky jsou narozdíl od knihovny cppcoro, kterou asi znáte, 'hot' - tedy přes TaskFactory je Task rovnou nastartován a naschedulován k vyřízení.
Task má API, které čekáte. A narozdíl od std::future má metodu ContinueWith (then).

Task se dá "co_awaitovat", protože podporuje concept "Awaiter". A můžete ho samozřejmě použít jako návratovou hodnotu z coroutine metody ("coroutine promise type").

Dále jsou v knihovně jednoduché metody pro vytvoření dokončeného Tasku z předpřipravené hodnoty (TaskFromResult), z výjimky (TaskFromException), nebo lze vrátit Task ve stavu Canceled (TaskFromCanceled).

Jednoduché DataFlow. ("flat", "fork-join" a a další typy).

Kombinátory pro Task:
WaitAll.
WaitAny.

TaskCompletionSouce - std:: promise bez těch otravných věcí, které určitě znáte sami.

Funkcionální kompozice tasku.
Fbind (alias bind, SelectMany, flatMap, mapMany)
Fmap (map, Select)

Asynchronní primitivy.
AsyncSemaphore.

Kooperativní storno pomocí tříd CancellationTokenSource a a CancellationToken.

AsyncProducerConsumerCollection.

Více zde:
https://github.com/renestein/Rstein.AsyncCpp
-------------------------------------------------------------------------------

Dear friends/followers,
maybe the result of my experimentation with coroutines may be interesting for someone else.
The RStein.AsyncCpp library (early unoptimized alpha) is a set of types that should be familiar for anyone who knows the Task Parallel Library (TPL) for .NET (C#). In addition, this library contains simple DataFlow, functional combinators for the Task class, useful async primitives (AsyncSemaphore, AsyncProducerConsumerCollection, CancellationToken, CancellationTokenSource ...).
The library is my playground for testing coroutine support in C++.
The library supports compilation in the VS 2019. Support for other compilers is planned.
More info.
https://github.com/renestein/Rstein.AsyncCpp



Monday, 04 May 2020 09:24:33 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C++ | Nativní kód


 Wednesday, 13 February 2019
ConfigureAwaitEnforcer – extenze pro Visual Studio

I když je tento blog nechutná a technicky zaostalá zombie, která vede k úvahám, jak konečně tuhle bestii zabít, aby mě už nestrašila, dá se přesto, nebo možná právě proto :), využít k šíření ConfigureAwait infekce v cizím kódu.  I tady by snad ještě někoho mohla zajímat moje extenze pro Visual Studio, která zkontroluje:

1) Jestli jste při použití ‘await someTask’ nezapomněli na ConfigureAwait(false).

Když jste tuhle chybu udělali, extenze dotyčný řádek jako prototypická labilní nervní učitelka červeně podtrhne a:

a) Nabídne úpravu výrazu přidáním ConfigureAwait(false).

b) Nabídne úpravu výrazu přidáním ConfigureAwait(true).

Nejlepší je extenzi vidět v akci.
yDitj9JOJh

Ve verzi 1.1 si můžete zvolit i závažnost diagnostiky (Error, Warning, Info, Hidden).

Analyzér je dostupný i na nugetu.
https://www.nuget.org/packages/ConfigureAwaitEnforcer/

Zdrojové kódy.

Bitbucket
https://bitbucket.org/renestein/configureawaitenforcer/src/master/

Github

https://github.com/renestein/ConfigureAwaitEnforcer



Wednesday, 13 February 2019 12:45:00 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C#


 Friday, 03 October 2014
Materiály z přednášky TPL - konkurenční, paralelní a asynchronní kód pro náročné pro WUG Praha

Prezentace:

Ad hoc příklady

https://bitbucket.org/renestein/wugtplex.ref.p/src

Knihovna Rstein.Async

https://bitbucket.org/renestein/rstein.async/src

Seriál o knihovně Rstein.Async

http://jdem.cz/ba8kp3

Cesta k příkladům s aktory v knihovně Rstein.Async

https://bitbucket.org/renestein/rstein.async/src/93fe127f35ac3b37d7ff31aa2a25d6a80fc1ce0b/RStein.Async.Examples/?at=master



Friday, 03 October 2014 11:23:00 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  


 Tuesday, 19 August 2014
Veřejná přednáška pro WUG - TPL – konkurenční, paralelní a asynchronní kód pro náročné.

Rád bych vás pozval na svou přednášku, kterou pořádá WUG.

Název přednášky: TPL - konkurenční, paralelní a asynchronní kód pro náročné.

Datum konání: 2.10.2014 od 17:30 do 21:00

Místo konání: pobočka: BB centrum, budova Alfa (Aquarius), Vyskočilova 1461/2a, Praha 4

Registrace na přednášku: http://wug.cz/praha/akce/597-TPL-konkurencni-paralelni-a-asynchronni-kod-pro-narocne

Anotace přednášky:

Znáte alespoň trochu Task Parallel Library a přednášek slibujících další nenáročný „úvod do TPL“ jste už viděli dost? Myslíte si, že klíčová slova async/await v C# jsou magií kompilátoru, jejíž kouzlo pro vás už navěky pominulo po zhlédnutí triviálních a donekonečna opisovaných příkladů, jak zavolat asynchronně pár nudných webových služeb?

Na přednášce probereme, jak rozšířit knihovnu TPL o další užitečné konstrukce i jak odstranit některá omezení v současné verzi TPL. Podíváme se na různé způsoby psaní konkurenčního, paralelního a asynchronního kódu. U konkurenčního kódu se zaměříme (nejen) na aktory a porovnáme různé způsoby, jak můžeme aktory psát.

Nezapomeňte s sebou vzít i kolegy, kteří hlásají, že každou nebezpečnou hlavu konkurenčního kódu setne jeden pořádný „lock“, a to nejlépe rekurzivní, aby vás deadlock nebo livelock ve firmě zabavil i o dlouhých zimních večerech.



Tuesday, 19 August 2014 11:08:00 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | C# | Návrhové vzory


 Wednesday, 25 June 2014
Task Parallel Library a RStein. Async 5 z n – Hrajeme si s ThreadPoolSchedulerem


Po napsání ThreadPoolScheduleru v předchozím díle následuje další slíbený oddychový díl, ve kterém si máme s ThreadPoolSchedulerem pohrát. Název je možná trochu zavádějící, protože nás žádné rozkošné hrátky nečekají.ThreadPoolScheduler  zcela pragmaticky otestujeme, abychom si potvrdili, že jde o plně funkční threadpool  a že se takový threadpool dá použít všude, ke je očekáván TPL scheduler.

Jako vždy připomenu, že knihovna RStein.Async, ve které naleznete i ThreadPoolScheduler, je dostupná  na Bitbucketu.
git clone git@bitbucket.org:renestein/rstein.async.git


Seriál  Task Parallel Library a RStein.Async  (předběžná osnova)

Task Parallel Library a RStein. Async 1 z n –  Popis základních tříd a obcházení omezení v TPL.

Task Parallel Library a RStein. Async 2 z n –  (boost) ASIO v .Net a IoServiceScheduler.

Task Parallel Library a RStein. Async 3 z n – Ukázky použití IoServiceScheduleru. Coroutines.

Task Parallel Library a RStein. Async 4 z n  – ThreadPoolScheduler založený na IoServiceScheduleru.

Task Parallel Library a RStein. Async 5 z n – Hrajeme si s ThreadPoolSchedulerem.

Task Parallel Library a RStein. Async 6 z n – Vytvoření StrandScheduleru.

Task Parallel Library a RStein. Async 7 z n – Náhrada za některé synchronizační promitivy – ConcurrentStrandSchedulerPair.

Task Parallel Library a RStein. Async 8 z n – Jednoduchý “threadless” actor model s využitím StrandScheduleru.

Task Parallel Library a RStein. Async 9 z n – Píšeme aktory I.

Task Parallel Library a RStein. Async 10 z n – Píšeme aktory II.

Task Parallel Library a RStein. Async 11 z n – Píšeme nový synchronizační kontext  - IoServiceSynchronizationContext.

Task Parallel Library a RStein. Async 12 z n – Použití IoServiceSynchronizationContextu v konzolové aplikaci a Windows službě.

(bude upřesněno)


Poznámka: V celé sérii článků budu používat slovo Task pro třídu, task pro název proměnné / argumentu metody a ”anglicismy” tásk/tásky místo “úloha/úlohy“ nebo jiného českého patvaru při zmínce o /úlohách-táscích/ v dalším textu. Předpokládám, že pro většinu vývojářů je takový text srozumitelnější.

Vytvoříme si ThreadPoolScheduler pro testy.

V metodě InitializeTest třídy IoServiceThreadPoolSchedulerTests vytvoříme IoServiceScheduler, předáme ho do konstruktoru ThreadPoolScheduleru a ThreadPoolScheduler poslouží k inicializaci  ProxyScheduleru.

ProxyScheduler našeho ThreadPoolScheduleru je TPL scheduler pro TaskFactory. Tásky vytvořené v testTaskFactory zpracuje ThreadPoolScheduler.

m_testTaskFactory = new TaskFactory(ProxyScheduler.AsTplScheduler());

Vše o vzájemných vztazích mezi “proxy” schedulery a “reálných” schedulery naleznete v prvním díle seriálu.

Přišla také chvíle, abych vysvětlil, proč existuje třída IAutonomousSchedulerTests a upřesnil terminologii v knihovně RStein.Async. Ve třídě IAutonomousSchedulerTests se nacházejí testy, kterými musí projít všechny autonomní schedulery. Jako autonomní scheduler označuju takový scheduler, který po svém vytvoření ihned zpracovává předané tásky a nevyžaduje ze strany aplikace již žádnou další konfiguraci ani podporu při zpracování tásků. Z schedulerů, které prozatím známe, můžeme za autonomní schedulery označit právě ThreadPoolScheduler nebo CurrentThreadScheduler, který jsme viděli v prvním díle seriálu. Naopak IoServiceScheduler není autonomní scheduler, protože po svém vytvoření čeká na to, až mu aplikace propůjčí pro vyřizování tásků thread tím, že zavolá jednu z jeho metod Run, RunOne, Poll nebo PollOne.

Nejprve otestujeme konstruktory ThreadPoolScheduleru.

Jestliže není předán IoServiceScheduler, musí být vyvolána výjimka.

Počet threadů v threadpoolu nesmí být nulový a ani neumíme vytvořit záporný počet threadů.

Jak jsem před chvílí vysvětlil, ThreadPoolScheduler je jedním z autonomích schedulerů,  a proto musí projít všemi testy pro autonomní schedulery. Většinu testů jsme viděli při testování CurrentThreadScheduleru, a proto  u následujících testů jen jen shrnu, že v jednom testu ověřujeme bezproblémové vyřízení jednoho tásku a v dalším testu zpracování většího množství tásků.

Následující test je zajímavější, protože ověřuje, že když zavoláme metodu Dispose, tak ThreadPoolScheduler vyřídí všechny zbývající tásky a teprvé poté metoda Dispose ukončí činnost scheduleru. O metodě Dispose u schedulerů chci ještě v nějakém dalším díle napsat více, protože deterministické ukončení činnosti různých schedulerů, kdy každý může mít zcela odlišné chování, je jeden z nejméně příjemných úkolů a bez ohledu na to, jaké řešení zvolíte, nezbavíte sebe ani ostatní, kteří schedulery ve svých aplikacích pouze používají, všech problémů. Jsem v čím dál silnějším pokušení některé hraniční scénáře, kdy klient nerespektuje životní cyklus schedulerů a tásků,  přesunout do temné říše nedefinovaného chování.Veselý obličej U ThreadPoolScheduleru ale nic takového nehrozí, i když byste měli mít nepříjemné mrazení z toho, že se snažíte likvidovat scheduler, aniž byste si byli jisti, že před voláním metody Dispose vyřídil všechny tásky.


Asynchronní test Dispose_When_Tasks_Are_Queued_Then_All_Tasks_Are_Executed spadá pod ty užitečné, ale ne zrovna bezpečné testy, o kterých jsem už také psal. Vygenerujeme tisíc tásků, zařadíme je ke zpracování, ale každý tásk je zablokován do té doby, dokud nestornujeme CancellationTokenSource s názvem waitForSignalCts. Za storno odpovídá metoda CancelAfter, která stornuje CancellationToken po uplynutí stanoveného času. My stornujeme CancellationToken po uplynutí jedné sekundy od zařazení tásků ke zpracování. Ihned po zavolání metody CancelAfter zavoláme metodu Dispose scheduleru a ověříme, že všechny vygenerované a scheduleru předané tásky byly zpracovány.

Kdyby to snad někomu ušlo, upozorním, že CancellationTokenSource používáme ke komunikaci mezi thready. Žádný tásk nestornujeme, jen použijeme CancellationToken k částečné synchronizaci mezi kódem v testu a kódem v táscích. Taková rychlá náhrada za synchronizační primitivu ManualResetEventSlim nebo její příbuzné.


Psal jsem, že test není bezpečný. V testu je totiž “race condition”, protože by teoreticky mohlo dojít k tomu, že metoda Dispose bude zavolána teprve poté, co jsou všechny tásky v scheduleru už vyřízeny. Takový test by opět prošel, ale ověřil  by jen chování, které už ověřuje test WithTaskFactory_When_Tasks_Are_Queued_Then_All_Tasks_Are_Executed. V této fázi vývoje knihovny RStein.Async dokážu i s takovým špinavým testem sdílet jednu “solution” ve Visual Studiu. Veselý obličej

Další testy už jsou pro všechny schedulery společné a můžete se na ně podívat sami.

Můžeme být spokojeni, všechny testy jsou zelené.

Image

V dalším díle si pořídíme zajímavý přírůstek do rodiny schedulerů s názvem StrandScheduler. Plným jménem StrandSchedulerDecorator.



Wednesday, 25 June 2014 04:30:00 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C# | Návrhové vzory


 Monday, 16 June 2014
Task Parallel Library a RStein. Async 4 z n – ThreadPoolScheduler založený na IoServiceScheduleru.

(Starší verze obnovena ze zálohy 21. 1. 2020.)


V předchozím díle o coroutines jsme poprvé viděli, jak se dá použít IoServiceScheduler. V tomto článku uvidíme, že pár metod IoServiceScheduleru stačí i k napsání jednoduchého threadpoolu. Tento článek i následující článek jsou oproti předchozím článkům kratší  a oddechové, abychom získali důvěrný vztah ke způsobu práce se schedulery v knihovně  RStein.Async, a nepřekvapil nás v šestém díle StrandScheduler, se kterým se vydáme mezi aktory.

Knihovna RStein.Async je dostupná  na Bitbucketu.
git clone git@bitbucket.org:renestein/rstein.async.git


Seriál  Task Parallel Library a RStein.Async  (předběžná osnova)

Task Parallel Library a RStein. Async 1 z n –  Popis základních tříd a obcházení omezení v TPL.

Task Parallel Library a RStein. Async 2 z n –  (boost) ASIO v .Net a IoServiceScheduler.

Task Parallel Library a RStein. Async 3 z n – Ukázky použití IoServiceScheduleru. Coroutines.

Task Parallel Library a RStein. Async 4 z n  – ThreadPoolScheduler založený na IoServiceScheduleru.

Task Parallel Library a RStein. Async 5 z n – Hrajeme si s ThreadPoolSchedulerem.

Task Parallel Library a RStein. Async 6 z n – Vytvoření StrandScheduleru.

Task Parallel Library a RStein. Async 7 z n – Náhrada za některé synchronizační promitivy – ConcurrentStrandSchedulerPair.

Task Parallel Library a RStein. Async 8 z n – Jednoduchý “threadless” actor model s využitím StrandScheduleru.

Task Parallel Library a RStein. Async 9 z n – Píšeme aktory I.

Task Parallel Library a RStein. Async 10 z n – Píšeme aktory II.

Task Parallel Library a RStein. Async 11 z n – Píšeme nový synchronizační kontext  - IoServiceSynchronizationContext.

Task Parallel Library a RStein. Async 12 z n – Použití IoServiceSynchronizationContextu v konzolové aplikaci a Windows službě.

(bude upřesněno)


Poznámka: V celé sérii článků budu používat slovo Task pro třídu, task pro název proměnné / argumentu metody a ”anglicismy” tásk/tásky místo “úloha/úlohy“ nebo jiného českého patvaru při zmínce o /úlohách-táscích/ v dalším textu. Předpokládám, že pro většinu vývojářů je takový text srozumitelnější.

ThreadPool z .Net Frameworku nebo threadpool z WIN32 API znáte. ThreadPool není v základu nic jiného než volné sdružení déle žijících threadů, které bylo založeno za účelem rychlého zpracování tásků. ThreadPool se používá hlavně proto, abychom v aplikace nevytvářeli a nelikvidovali thready, jak se nám zlíbí, protože thready nejsou zrovna “laciné” objekty na vytvoření, správu ani likvidaci, Jak jsem již v seriálu zmínil, TaskScheduler.Default využívá k vyřizování tásků standardní .Net ThreadPool.

Náš threadpool bude oproti třídě ThreadPool v .Net Frameworku velmi jednoduchý. Hlavním rozdílem bude to, že nebudeme podporovat “work stealing queue”, i když by nebyl příliš velký problém takovou podporu dopsat.

Dalším podstatným rozdílem oproti threadpoolu v .Net frameworku  je to, že náš threadpool bude k vyřizování tásků od svého vytvoření až do svého zrušení  používat fixní a po celou dobu svého života stejný počet threadů. V .Net threadpoolu se může počet threadů měnit. .Net threadpool přidá thread mj. v situaci, kdy předpokládá, že mohlo dojít k deadlocku.

Kdy může v threadpoolu dojít k deadlocku?

  1. Představme si, že v threadpoolu je 10 threadů.
  2. Všech 10 threadů je používáno a vyřizuje nějaké tásky. 9 threadů v threadpoolu čeká na dokončení tásku v threadu  s číslem 10.
  3. Tásk v threadu 10 zařadí ke zpracování v threadpoolu další tásk (tásk 11) a čeká na jeho dokončení.
  4. Všech 10 threadů je vytíženo, tásk s pořadovým číslem 11 nebyl ještě spuštěn. Máme klasický příklad deadlocku, protože na dokončení tásku 11 čeká tásk v threadu 10 a na dokončení tásku v threadu 10 čekají tásky v threadech 1-9.  Snad jen Coffman by měl z takové situace radost.

Heuristika .Net threadpoolu po nějakém čase iniciuje vytvoření další threadu, protože je zřejmé, že všechny thready v threadpoolu jsou využity, ale žádné tásky nebyly již “delší dobu” dokončeny. Nově vytvořený thread vyřídí tásk s číslem 11, tím se uvolní tásk v threadu 10 a doběhnou i tásky v threadech 1-9. Voilà, deadlock byl odstraněn. Samozřejmě že tohle je jen jedna z variací mnoha zhoubných scénářů, které si asi všichni dokážeme představit. Tásk v threadu 11 by mohl třeba vygenerovat tásky 12-20 a čekat na jejich dokončení a threadpool by chtě nechtě přidával další a další thready.

Já tohle chování threadpoolu, které se na první pohled může zdát jako vstřícné a bezproblémové, nepovažuju za moc vhodné, protože threadpool jím kamufluje po dlouhou dobu některé nepříjemné chyby v logice aplikace a toleruje i velmi těsné a nevhodné vztahy mezi tásky. Náš threadpool nikdy po počáteční inicializaci fixního počtu threadů žádný další thread nepřidá. A to ani tehdy, jestliže je mu předán tásk, u něhož je nastaven příznak LongRunning. Jestliže to někomu vadí, pull request je vítán.

IoServiceThreadPoolScheduler staví na konstrukcích pro schedulery, které jsme si vysvětlili v prvním díle.

Image

Na IoServiceThreadPoolScheduleru je nejzajímavější, že nám k napsání threadpoolu stačí pár řádků. Většinu práce opět odvede IoServiceScheduler. Snad teď už začíná být zřejmé, proč jsem IoServiceScheduleru věnoval tak dlouhý díl.

  • IoServiceThreadPoolScheduler vyžaduje v konstruktoru instanci IoServiceScheduleru a počet threadů, které budou tvořit threadpool. Jestliže počet threadů neurčíme, vytvoří se počet threadů shodný s počtem dostupných logických procesorů. 
  • V privátní metodě initThreads volané z konstruktoru thready vytvoříme a ihned je propůjčíme IoServiceScheduleru tím, že zavoláme jeho metodu Run. Každému threadu také přidělíme jméno a nastavíme u něj příznak IsBackground na true, protože thready v threadpoolu by neměly blokovat ukončení procesu.
    Již v konstruktoru IoServiceThreadPoolScheduleru jsme vytvořili objekt Work, o kterém víme, že zajistí, aby metoda Run nevrátila řízení, ani když žádné tásky neexistují a thread tedy neskončí. Musíme si poradit jen s tím, že při vyřizování některého tásku dojde k výjimce. Jestliže aplikace běží pod debuggerem, díky metodě Debugger.Break může autor aplikace začal pátrat po příčině kritické chyby, jinak odstřelíme aplikaci voláním metody Environment.FailFast. Mrtvý proces je kupodivu lepší proces než proces s narušenými daty a/nebo vyšinutou logikou zpracování.
  • V metodě Dispose nejprve zrušíme objekt Work a počkáme, až všechny thready v threadpoolu vyřídí všechny zbývající tásky a skončí svou činnost. IoServiceThreadPoolScheduler považuju instanci IoServiceScheduleru za instanci, kterou exkluzivně vlastní, a proto i na ní zavolá metodu Dispose.
  • Metody QueueTask, TryExecuteTaskInline a GetScheduledTasks, které musí mít každý scheduler v knihovně RStein.Async, jen delegují na stejně nazvané metody IoServiceScheduleru. IoServiceThreadPoolScheduler, který bude z hlediska aplikace viditelným schedulerem pro zpracování tásků, také zajistí, že předaný ProxyScheduler bude používat i podkladový a pro aplikaci jinak neviditelný IoServiceScheduler. Opět podotknu, že kdyby někdo tápal, proč používám ProxyScheduler, v prvním  díle seriálu jsou “proxy” schedulery i “reálné” schedulery podrobně popsány včetně důvodů pro zavedení této distinkce mezi schedulery.
  • Možná někoho z vás zarazilo, že náš threadpool nemá žádnou metodu QueueUserWorkItem, kterou asi znáte z .Net threadpoolu. Napsání takové statické metody je jednoduché, ale různé instance ThreadPoolScheduleru mohou být využívány jako samostatné a navzájem na sobě nezávislé TPL schedulery v různých částech aplikace a neomezují nás tím, že bychom měli v jedné aplikační doméně jen jeden threadpool, do kterého statická metoda QueueUserWorkItem beze všech skrupulí hází všechny tásky.

Příště se podíváme na testy, ze kterých vyplyne, jak má být ThreadPoolScheduler používán, a proč metodu QueueUserWorkItem nepotřebujeme.



Monday, 16 June 2014 06:06:00 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C#


 Monday, 09 June 2014
Task Parallel Library a RStein. Async 3 z n – Ukázky použití IoServiceScheduleru. Coroutines.

(Starší verze obnovena ze zálohy 21. 1. 2020.)


V předchozím dílu seriálu o TPL a knihovně RStein.Async a knihovně jsme napsali IoServiceScheduler.

Dnes se podíváme,  jak se dají s IoServiceSchedulerem napsat tzv. “coroutines” .

Knihovna RStein.Async je dostupná  na Bitbucketu.
git clone git@bitbucket.org:renestein/rstein.async.git


Seriál  Task Parallel Library a RStein.Async  (předběžná osnova)

Task Parallel Library a RStein. Async 1 z n –  Popis základních tříd a obcházení omezení v TPL.

Task Parallel Library a RStein. Async 2 z n –  (boost) ASIO v .Net a IoServiceScheduler.

Task Parallel Library a RStein. Async 3 z n – Ukázky použití IoServiceScheduleru. Coroutines.

Task Parallel Library a RStein. Async 4 z n  – ThreadPoolScheduler založený na IOServiceScheduleru.

Task Parallel Library a RStein. Async 5 z n – Hrajeme si s ThreadPoolSchedulerem.

Task Parallel Library a RStein. Async 6 z n – Vytvoření StrandScheduleru.

Task Parallel Library a RStein. Async 7 z n – Náhrada za některé synchronizační promitivy – ConcurrentStrandSchedulerPair.

Task Parallel Library a RStein. Async 8 z n – Jednoduchý “threadless” actor model s využitím StrandScheduleru.

Task Parallel Library a RStein. Async 9 z n – Píšeme aktory I.

Task Parallel Library a RStein. Async 10 z n – Píšeme aktory II.

Task Parallel Library a RStein. Async 11 z n – Píšeme nový synchronizační kontext  - IoServiceSynchronizationContext.

Task Parallel Library a RStein. Async 12 z n – Použití IoServiceSynchronizationContextu v konzolové aplikaci a Windows službě.

(bude upřesněno)


Poznámka: V celé sérii článků budu používat slovo Task pro třídu, task pro název proměnné / argumentu metody a ”anglicismy” tásk/tásky místo “úloha/úlohy“ nebo jiného českého patvaru při zmínce o /úlohách-táscích/ v dalším textu. Předpokládám, že pro většinu vývojářů je takový text srozumitelnější.

Nejdříve krátké vysvětlení, co jsou “coroutines”.

Metody/funkce určitě dobře znáte, a proto víte, že do metody vstoupíte, metoda udělá svou práci a pak skončí. Metoda nemusí skončit úspěšně, může dojít k vyvolání výjimky, v průběhu může metoda volat další metody, ale klíčové je, že metodu aktivujeme, metoda udělá svou práci a skončí. Spustíme metodu, metoda udělá svou práci a skončí. Start a pak Stop. Když metodu zavoláme znovu, metoda se provede vždy od prvního řádku.

Coroutines jsou zajímavější stvoření. Nejlépe si je můžeme představit jako metody, které se dají dočasně pozastavit, a po chvíli je můžeme znovu spustit od místa, kde jsme je pozastavili. A pak je můžeme pozastavit znovu. Jako když na dálkovém ovladači  pozastavíte rozkoukaný film, odběhnete na WC, pustíte film znovu, pak pozastavíte film, abyste odpověděli manželce/přítelkyni/dětem/psovi na nějaký záludný dotaz/skřek/štěk, spustíte film a užijete si závěrečné titulky. A stejně jako se při pozastavení filmu uloží místo, kde jste skončili, abyste se nemuseli dívat po každé pauze na film od začátku, u coroutine uložíme  stav metody (hodnoty proměnných), abychom po krátké “pauze” mohli bez problémů pokračovat na dalších řádcích metody.

Coroutine:

Start metody, Pauza a uložení stavu, Start na uložené pozici  a obnovení stavu, Pauza a uložení stavu, Start na uložené pozici a obnovení stavu, Pauza a uložení stavu, […], Stop metody.

“Klasická” Metoda:

Start metody, Stop metody.

Z předcházejících řádků by mělo vyplynout, proč se o klasických metodách/funkcích (“subroutine”) občas mluví jako o speciálních nebo degenerovaných případech coroutine. Subroutine se od coroutine liší tím, že mezi spuštěním a ukončením  metody není žádná pauza.

K čemu se dají coroutines využít?

”Coroutines” se dají mj. považovat za lehkotonážní náhradu threadů. I když by asi bylo lepší napsat, že coroutines jsou odlehčeným doplňkem threadů než jejich náhradou. Thready jako základní jednotku konkurence v operačním systému Windows můžeme při použití TPL sice ignorovat, ale to neznamená, že neexistuje. A znáte to – kdo nezná svou historii, je nucen ji prožít znovu a zopakovat si s TPL tásky chyby, které jsou známy už z naivní práce s thready. Veselý obličej

Pomocí coroutines můžete v aplikaci podporovat kooperativní multitasking. Kooperativní proto, že každá coroutine se v nějakém okamžiku sama vzdá řízení (bude “zapauzována”) a předá řízení své kolegyni. Thready podporují preemptivní multitasking – zjednodušeně řečeno, o jejich spuštění, pozastavení  a času, který je threadu poskytnut,  rozhoduje operační systém.

Lehkotonážní jsou coroutines proto, že na jeden thread v operačním systému můžeme namapovat libovolné množství “coroutines”. Adjektivum lehkotonážní u coroutines  vyjadřuje také to, že při kooperativím přepnutí řízení z jedné coroutine do druhé se vyhneme relativně drahému  “context switchi” a opakovanému přechodu z “user” módu do ”kernel” módu, ke kterému může dojít, když koordinujeme postup threadů v aplikaci pomocí primitiv Semaphor, ManualResetEvent(Slim), .AutorResetEvent atp.

Coroutines mohou být zajímavé i proto, že když dovolíme, aby v nějaké množině coroutines běžela v každém časovém okamžiku maximálně jedna coroutine, pak si můžeme být jisti, že i když tisíc různých coroutines modifikuje sdílený stav, který není v kritické sekci a ani k němu není řízen přístup pomocí jiné synchronizační primitivy, tak se nic neděje a ke ke korupci dat nemůže dojít, protože se stavem nikdy nepracujeme současně z více threadů/coroutines.

V tomto článku si napíšeme podporu pro jednoduché coroutines, které budou na sobě vzájemně nezávislé, nebudou si předávat žádná data (nepůjde tedy o coroutines s podporou pull/push hodnot) a jen zajistíme, že poté, co bude jedna coroutine “zapauzována”, tak se ihned spustí následující coroutine.

Od C# 2.0 bylo možné psát “coroutines” pomocí kontextového slova yield. I když jste možná yield k psaní coroutine nikdy nepoužili, můžete se na pěkný příklad takových coroutines podívat v MVVM framoworku Caliburn.Micro.

Po přidání klíčových slov async a await do C#, je vytváření coroutine mnohem snazší, protože veškerou těžkou práci odvede kompilátor.

Kdykoli v async metodě použijeme klíčové slovo await, tak kompilátor zaručí, že poté, co awaiter tvrdí, že tásk ještě není dokončen, tak je metoda pozastavena, je uložen současný stav metody a metoda je “roztržena” na dvě části. Ještě neprovedený kód metody a současný stav metody je uložen do “continuation” delegáta, který je vyvolán  “později”, když je tásk dokončen. V delegátu “continuation” je kód, který musí být proveden po “odpauzování” metody. Mým cílem není teď v článku vysvětlovat všechny nuance klíčových slov async, await ani awaiterů, to bychom popsali hodně listů na blogu, ale jen zrekapituovat a akcentovat, že kompilátor je schopen pozastavit provádění metody a požádat awaiter, aby někdy později zbývající kód v metodě spustil.

Metoda spuštěna – metoda “zapauzována” (magie kompilátoru), je uložen stav metody (delegát continuation) a continuation je předána awaiteru – awaiter “odpauzuje” metodu (vyvolá  continuation) - metoda “zapauzována” (magie kompilátoru), je uložen stav metody (delegát continuation) a continuation je předána awaiteru – awaiter “odpauzuje” metodu (vyvolá  continuation) – […]  - Metoda ukončena.

Tento rytmus zpracování async metody se shoduje s rytmem životního cyklu coroutine.

Start metody, Pauza a uložení stavu, Start na uložené pozici  a obnovení stavu, Pauza a uložení stavu, Start na uložené pozici a obnovení stavu, Pauza a uložení stavu, […], Stop metody.

Na nás je jen zaručit, že coroutines podporují kooperativní multitasking.

Když bude chtít jedna metoda předat řízení jiné metodě, stačí, aby zavolala await na objektu coroutine, který metodám předáme jako argument.

await coroutine; //=vzdávám se řízení, přišel čas předat vládu jiné metodě.

Napíšeme si tedy třídu Coroutine, která se spolehne na služby IoServiceScheduleru  a která má roli awaiteru.

Každý awaiter by měl podporovat rozhraní INotifyCompletion, a proto tak činí i naše třída Coroutine.

Coroutine předáme odkaz na IoServiceScheduler a IoServiceScheduler také zastane veškerou práci.

Náš awaiter z metody IsCompleted vrací false, protože potřebujeme, aby každá metoda, která zavolá await, byla zapauzována – vrácení hodnoty false je příkazem, aby byl vygenerován delegát continuation. Awaiter dostane objekt continuation jako argument metody onCompleted.  Jediné, co uděláme, je, že delegáta předáme do metody Post IoServiceScheduleru. Připomeňme, že metoda Post  v IoServiceScheduleru vytvoří z předaného delegáta tásk a tento tásk je zařazen ke zpracování na konec fronty, takže dáme šanci pokročit ve zpracování i dalším metodám.

Metoda Run u třídy Coroutine jen deleguje na metodu Run IoServiceScheduleru. v propůjčeném se budou střídat vykonání jednotlivých metod-coroutines.


Co nám ještě chybí? Metody, na kterých si ukážeme, jak si coroutines předávají řízení a také kód, který vytvoří IoServiceScheduler pro koordinující objekt Coroutine (awaiter) a dovolí jednotlivé metody-coroutines spustit.

Metoda-konkrétní coroutine.

Metoda Start dostane odkaz na objekt Coroutine a vypíše nám informace, kde se při svém zpracování nachází. My můžeme argumentem numberOfIterations předaným do konstruktoru ovlivnit počet iterací metody.

Všimněte si také, že await volám nejen na argumentu coroutine, ale i na TPL metodách Task.Yield a Task.Delay. I u nich by mělo platit, že se metoda vzdá dočasně řízení, spustí se jiná coroutine a po nějaké době bude metoda znovu spuštěna, aniž by její běh interferoval s během jiných metod–coroutines. U nás tomu tak skutečně bude, ale spíš jde o šťastnou souhru okolností.

Jestliže zavoláte await, objekt continuation je spuštěn v zachyceném synchronizačním kontextu, anebo, když synchronizační kontext neexistuje, tak se použije současný scheduler, a když ani ten neexistuje, tak se použije TaskScheduler.Default. My spustíme všechny metody přes IoServiceScheduler, který používá i třída Coroutine – protože v konzolové aplikaci synchronizační kontext není, použije se dostupný scheduler, a to je náš IoServiceScheduler. Všechny objekty “continuation” tak nakonec skutečně budou spuštěny v threadu IoServiceScheduleru. Abychom nebyli v podobných situacích závislí na nahodilé souhře okolností, napíšeme si v jedenáctém díle seriálu nový synchronizační kontext – IoServiceSynchronizationContext.

Zbývá dopsat spuštění coroutines.

  1. V konstruktoru vytvoříme scheduler a předáme ho instanci třídy Coroutine.
  2. V metodě Start přidáme coroutine voláním metody addCoroutines, ve které si můžete pohrát s nastavením, kolik coroutines vznikne (konstanta NUMBER_OF_COROUTINES) a kolik iterací cyklu (konstanta NUMBER_OF_ITERATIONS), který jsme viděli výše,  provede  každá coroutine. Coroutines připravíme ke spuštění přes metodu Post IoServiceScheduleru. Do IoServiceScheduleru přidáme také tásk, který se spustí v momentě, kdy všechny coroutines doběhnou, a tento tásk odstraní (Dispose) objekt Work, o kterém víme, že udrží metodu Run IoServiceScheduleru v chodu, i když zrovna v IoServiceScheduleru nejsou ve frontě žádné tásky. Objekt Work potřebujeme, protože naše coroutine používají metodu Task.Delay, takže by se mohlo stát, že IoServiceScheduler nezpracovává žádné tásky-continuation, protože všechny coroutines čekají na dokončení metody Delay.
  3. Metoda Start zavolá Run na IoServiceScheduleru. První coroutine se díky IoServiceScheduleru rozeběhne.

Spuštění našich testovacích coroutines:

Je vidět, jak se jednotlivé coroutines střídají při zpracování a jak si také předávají právo použít jeden jediný thread (tid ve výpise), který jsme propůčili IoServiceScheduleru. 

Image

Coroutines jsou tedy z určitého úhlu pohledu logické thready, které nyní mapujeme na jeden fyzický thread.

V tomto příspěvku jsem chtěl, abychom viděli, jak málo stačí k napsání coroutine v moderním C#, který je doplněn službami IoServiceScheduleru. Bylo by snadné rozšířit příklad o coroutines, které si nejen předávají řízení, ale které si i vyměňují informace. Pro vážné zájemce o coroutines také doplním, že na OS Windows jsou dostupné objekty Fiber. Přesto skepticky dodám, že jsem na moderním HW nenašel moc důvodů, proč objekty Fiber používat, a myslím si, že význam měly hlavně v době, kdy “context switch” byl u staršího HW nejen relativně, ale po změření celkové časové náročností operace i absolutně velmi drahý.

V příští části nás čeká ThreadPoolScheduler.



Monday, 09 June 2014 05:32:00 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C#


 Tuesday, 03 June 2014
Task Parallel Library a RStein. Async 2 – (boost) ASIO v .Net a IoServiceScheduler.
(Článek obnoven ze zálohy 21. 1. 2020, omlouvám se za formátování kódu.)

V dnešním příspěvku o TPL a knihovně RStein.Async napíšeme slibovaný IoServiceScheduler. Jestliže jste si ještě neprošli  první díl seriálu a nemáte přehled o tom, k čemu slouží “proxy” schedulery a “reálné” schedulery, jaká omezení z TPL jsem obešel a proč jsem zavedl tuto na první pohled podivnou terminologii, bude lepší, když si první díl přečtete dříve, než se začtete do dalších částí seriálu.
Knihovna RStein.Async je dostupná  na Bitbucketu.
git clone git@bitbucket.org:renestein/rstein.async.git

Také si hned v úvodu dovolím podotknout, že tento díl je velmi dlouhý, a optimisticky dodám, že ostatní díly seriálu by měly být o dost kratší.Veselý obličej


Seriál  Task Parallel Library a RStein.Async  (předběžná osnova)

Task Parallel Library a RStein. Async 1 z n –  Popis základních tříd a obcházení omezení v TPL.

Task Parallel Library a RStein. Async 2 z n –  (boost) ASIO v .Net a IoServiceScheduler.

Task Parallel Library a RStein. Async 3 z n – Ukázky použití IoServiceScheduleru. Coroutines.

Task Parallel Library a RStein. Async 4 z n  – ThreadPoolScheduler založený na IoServiceScheduleru.

Task Parallel Library a RStein. Async 6 z n – Vytvoření StrandScheduleru.

Task Parallel Library a RStein. Async 7 z n – Náhrada za některé synchronizační promitivy – ConcurrentStrandSchedulerPair.

Task Parallel Library a RStein. Async 8 z n – Jednoduchý “threadless” actor model s využitím StrandScheduleru.

Task Parallel Library a RStein. Async 9 z n – Píšeme aktory I.

Task Parallel Library a RStein. Async 10 z n – Píšeme aktory II.

Task Parallel Library a RStein. Async 11 z n – Píšeme nový synchronizační kontext  - IOServiceSynchronizationContext.

Task Parallel Library a RStein. Async 12 z n – Použití IOServiceSynchronizationContextu v konzolové aplikaci a Windows službě.

(bude upřesněno)


Poznámka: V celé sérii článků budu používat slovo Task pro třídu, task pro název proměnné / argumentu metody a ”anglicismy” tásk/tásky místo “úloha/úlohy“ nebo jiného českého patvaru při zmínce o /úlohách-táscích/ v dalším textu. Předpokládám, že pro většinu vývojářů je takový text srozumitelnější.

IoServiceScheduler byl pojmenován na počest své starší příbuzné io_service v  knihovně Boost.ASIO. I když převezmeme mnoho rysů z io_service, nebudeme otrocky kopírovat všechny její vlastnosti. Pro vývojáře, kteří io_service znají, podotknu, že náš IoServiceScheduler nelze považovat za plnohodnotnou implementaci vzoru Proactor. Bylo by sice snadné zavést v .Net Frameworku novou konvenci pro zpracování asynchronních IO operací i registrovat nové asynchronní poskytovatele a imitovat tak většinu rysů z Boost.Asio, ale protože v .Net Frameworku máme jiné idiomy, šlo by o zbytečné nošení cizorodého kódu do hájemství Microsoftu. Mrkající veselý obličej

IoServiceScheduleru charakterizuje to, že máme dokonale pod kontrolou, které thready zpracují vytvořené tásky. Dokud IoScheduleru nepropůjčíme thread tím, že zavoláme jednu z jeho metod Run, RunOne, Poll nebo PollOne, žádné tásky zpracovávány nebudou. Řečeno mírně jinak, použití IoServiceScheduleru v aplikaci zaručuje, že tásky nebudou vyřízeny jiným threadem, než tím, který IoServiceScheduleru výslovně a na dobu určitou propůjčíme. Samo o sobě nevypadá takové chování jako žádný zázrak, ale v průběhu celého seriálu zjistíme, jak na správném chování IoServiceScheduleru závisí další třídy .

Nejdříve se podíváme na rozhraní IoServiceScheduleru, které vychází z io_service, a já se pokusím stručně popsat, jak jednotlivé metody pracují. Poté se podíváme na testy, které ověřují korektní chování metod v IoServiceScheduleru, a napíšeme samotné metody IoServiceScheduleru.
Metody odpovědné za vyřizování tásků v IoServiceScheduleru:

Metoda int Run()
Po zavolání metody Run IoServiceScheduler začne v aktuálním threadu  vyřizovat tásky. Metoda Run skončí teprve tehdy, když začne platit jedna z uvedených podmínek:
1) IoServiceScheduler již neobsahuje žádné další tásky ke zpracování a současně jsme IoServiceScheduleru nepředali žádný (viz níže v článku) objekt “Work”, kterým sdělujeme, že metoda Run má čekat na další tásky do té doby, dokud objekt “Work” nezlikvidujeme. Objekt Work nyní  zjednodušeně berme jako vytížení IoServiceScheduleru nějakým táskem – předstíranou prací, která udrží metodu Run v zápřahu a nedovolí jí skončit.
2) IoServiceScheduler již neobsahuje žádné další tásky ke zpracování a dříve předaný objekt Work jsme zlikvidovali (=zavolali jsme jeho metodu Dispose).
3) Zavolali jsme metodu Dispose, kterou si vynutíme ukončení činnosti IoScheduleru.
Metoda vrátí počet zpracovaných tásků.
Počet threadů, které mohou v jednom okamžiku zavolat metodu Run, není omezen.

Metoda int RunOne()
Metoda RunOne vyřídí v aktuálním threadu právě jeden tásk. Jestliže IoServiceScheduler žádný tásk neobsahuje, metoda RunOne zablokuje aktuální vlákno do té doby, dokud nějaký tásk není IoServiceScheduleru předán.
Metoda RunOne tedy skončí svou činnost, když nastane jedna z těchto podmínek.
1) Metoda vyřídila právě jeden tásk.
2) Zavolali jsme metodu Dispose, kterou si vynutíme ukončení činnosti IoScheduleru.
Metoda vrátí počet zpracovaných tásků , návratová hodnota by měla být vždy rovna jedné.
Počet threadů, které mohou v jednom okamžiku zavolat metodu RunOne, není omezen.

Metoda int Poll()
Po zavolání metody Poll IoServiceScheduler začne v aktuálním threadu  vyřizovat tásky.
Metoda Poll skončí  tehdy, když začne platit jedna z uvedených podmínek:
1) IoServiceScheduler již neobsahuje žádné další tásky ke zpracování.
2) Zavolali jsme metodu Dispose, kterou si vynutíme ukončení činnosti IoScheduleru.
Na rozdíl od metody Run, metoda Poll skončí ihned poté, co zjistí, že již žádné další tásky v IoServiceScheduleru nejsou. Existence/absence objektu Work nemá na činnost metody Poll žádný vliv.
Metoda vrátí počet zpracovaných tásků .
Počet threadů, které mohou v jednom okamžiku zavolat metodu Poll, není omezen.

Metoda int PollOne()
Metoda PollOne vyřídí v aktuálním threadu maximálně jeden tásk. Jestliže IoServiceScheduler žádný tásk neobsahuje, metoda PollOne ihned ukončí svou činnost a vrátí řízení volajícímu kódu.
Metoda PollOne skončí svou činnost, když nastane jedna z těchto podmínek.
1) Metoda vyřídila právě jeden tásk.
2) Scheduler v době volání metody PollOne neobsahuje žádný tásk.
2) Zavolali jsme metodu Dispose, kterou si vynutíme ukončení činnosti IoScheduleru.
Na rozdíl od metody RunOne metoda PollOne nikdy neblokuje aktuální vlákno tím, že by čekala na zařazení nového tásku ke zpracování v IoServiceScheduleru.
Metoda vrátí počet zpracovaných tásků.  Návratová hodnota by měla být vždy 0 (tásků), nebo 1 (tásk).
Počet threadů, které mohou v jednom okamžiku zavolat metodu PollOne, není omezen.

IoServiceScheduler je potomkem TaskSchedulerBase a k zařazení i zpracování tásků v Scheduleru nabízí nám dobře známé metody.
public override void QueueTask(Task task){…};

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) {…}

Bez problémů tedy můžeme IoServiceScheduler předat do TaskFactory z TPL, podobně jako jsme si to již předvedli minule u CurrentThreadScheduleru.

IoServiceScheduler navíc nabízí k vytváření tásků  a jejich zařazení ke zpracování i alternativní rozhraní svého předka z Boost.Asio.

Metoda Task Dispatch(Action action);

Metoda z delegáta v argumentu action vytvoří nový tásk a připraví ho ke zpracování.

Jestliže je volána metoda Dispatch ve stejném threadu, ve kterém je nyní aktivní metoda Run, RunOne, Poll, nebo PollOne, tak metoda Dispatch může tásk vykonat ihned (“inline”).

Jestliže tedy stávající tásk vyřizovaný IoServiceSchedulerem zavolá metodu Dispatch, může být delegát action zavolán ihned, protože si můžeme být jisti, že tásk vykonáme ve “správném” threadu, který byl propůjčen IoServiceScheduleru.
Metoda vrátí  tásk, který je dokončen, když svou činnost skončí delegát v argumentu action.

Metoda Task Post(Action action);
Stejně jako metoda Dispatch, i metoda Post z delegáta v argumentu action vytvoří nový tásk a připraví ho ke zpracování.
Na rozdíl od metody Dispatch a bez ohledu na to, ve kterém threadu je metoda Post aktivována, metoda Post nesmí nikdy delegáta action vykonat ihned (“inline”), ale musí jen vytvořit nový tásk, zařadit ho ke zpracování a vrátit řízení.

Metoda vrátí tásk, který je dokončen, když svou činnost skončí delegát v argumentu action.

Metoda Action Wrap(Action action);
Metoda Wrap přebírá i vrací argument typu Action. Argument action je “zabalen” do delegáta, který po svém vyvolání argument action předá metodě Dispatch, o níž už víme, že z delegáta action vytvoří nový tásk ke zpracování.
Metodě Wrap  předáte kdykoli v delegátu action odkaz na  kód, u kterého požadujete, abyste ho mohli sami později zařadit ke zpracování v tomto IoServiceScheduleru ve formě tásku, a ona vám vrátí delegáta se stejnou signaturou, jakou má argument action , a který po svém vyvolání přesně toto zvládne.

Metody Dispatch, Post a Wrap mají přetížené varianty, které místo delegáta typu Action přijímají odkaz na delegáta typu Func<Task>. Tyto varianty existují proto, abychom se částečně zbavili  některých nepříjemných problémů s async lambda výrazy, které skvěle popsal Stephen Toub na MSDN blogu.

Jak jsem již poznamenal, metody Dispatch, Post a Wrap představují alternativní rozhraní pro vytváření tásků, a protože toto rozhraní bude mít odlišné klienty, než rozhraní známé z TPL, vzpomeneme si na princip “Interface Seggregation“ a zmíněné metody extrahujeme do samostatného rozhraní s názvem IAsioTaskService.

using System;

using System.Threading.Tasks;

namespace RStein.Async.Schedulers

{

public interface IAsioTaskService : IDisposable

{

Task Dispatch(Action action);

Task Dispatch(Func<Task> function);

Task Post(Action action);

Task Post(Func<Task> function);

Action Wrap(Action action);

Action Wrap(Func<Task> function);

}

}

Pro ty, kdo mají raději obrázky, zde je rozhraní třídy IoServiceScheduler znovu.

image

Po nezbytném úvodu bychom měli mít mnohem lepší představu o odpovědnostech IoServiceScheduleru a nyní si zkusíme IoService Scheduler napsat.

public class IoServiceScheduler : TaskSchedulerBase, IAsioTaskService

{

public const int REQUIRED_WORK_CANCEL_TOKEN_VALUE = 1;

public const int POLLONE_RUNONE_MAX_TASKS = 1;

public const int UNLIMITED_MAX_TASKS = -1;

private readonly ThreadLocal<IoSchedulerThreadServiceFlags> m_isServiceThreadFlags;

private readonly CancellationTokenSource m_stopCancelTokenSource;

private readonly BlockingCollection<Task> m_tasks;

private readonly object m_workLockObject;

private CancellationTokenSource m_workCancelTokenSource;

private volatile int m_workCounter;

public IoServiceScheduler()

{

m_tasks = new BlockingCollection<Task>();

m_isServiceThreadFlags = new ThreadLocal<IoSchedulerThreadServiceFlags>(() => new IoSchedulerThreadServiceFlags());

m_stopCancelTokenSource = new CancellationTokenSource();

m_workLockObject = new object();

m_workCounter = 0;

}

.....

}

IoServiceScheduler je potomkem naší bázové třídy TaskSchedulerBase a podporuje rozhraní IAsioTaskService. Po předchozích odstavcích určitě nejste překvapeni. Veselý obličej

V konstruktoru inicializujeme několik důležitých proměnných. V threadově bezpečné kolekci m_tasks typu BlockingTaskCollection budeme držet tásky zařazené ke zpracování. V threadově lokální proměnné  m_isServiceThreadFlags uložíme pro každý thread, který bude IoServiceScheduleru propůjčen, informaci, že jde o thread, který IoServiceScheduler po volání metod Poll, PollOne, Run a RunOne nyní vlastní, dále informaci o tom, kolik tásků můžeme v tomto threadu nyní vyřídit a kolik již jich bylo vyřízeno. Celá třída IoServiceSchedulerThreadFlags vypadá takto.

namespace RStein.Async.Schedulers

{

public class IoSchedulerThreadServiceFlags

{

public IoSchedulerThreadServiceFlags()

{

ResetData();

}

public bool IsServiceThread

{

get;

set;

}

public int MaxOperationsAllowed

{

get;

set;

}

public int ExecutedOperationsCount

{

get;

set;

}

public void ResetData()

{

IsServiceThread = false;

MaxOperationsAllowed = ExecutedOperationsCount = 0;

}

}

}

Vraťme se zpátky ke konstruktoru IoServiceScheduleru. Proměnná m_stopCancelTokenSource je instance CancellationTokenSource, která je stornována ihned poté, co je činnost IoServiceScheduleru voláním metody Dispose ukončena.

Proměnné m_workLockObject a m_workCounter se týkají objektů Work, které jsem letmo popisoval výše u metody Run. Zopakujme, že objekt Work představuje “práci”, která udrží metodu Run IoServiceScheduleru v chodu, i když IoServiceScheduler neobsahuje žádné tásky, a zprostředkovaně tak dosáhne toho,že si IoServiceScheduler ponechá jednou propůjčený thread i pro tásky, které mohou být do IoServiceScheduleru přidány “později”.

Ještě lepší asi bude, když se podíváme, jak je objekt Work udělán.

using System;

using System.Collections.Concurrent;

using System.Collections.Generic;

using System.Threading;

namespace RStein.Async.Schedulers

{

public sealed class Work : IDisposable

{

private readonly CancellationTokenSource m_cancelTokenSource;

public Work(IoServiceScheduler scheduler)

{

m_cancelTokenSource = new CancellationTokenSource();

scheduler.AddWork(this);

}

internal CancellationToken CancelToken

{

get

{

return m_cancelTokenSource.Token;

}

}

internal void RegisterWorkDisposedHandler(Action action)

{

m_cancelTokenSource.Token.Register(action);

}

public void Dispose()

{

Dispose(true);

}

private void Dispose(bool disposing)

{

if (disposing)

{

m_cancelTokenSource.Cancel();

}

}

}

}

Kdo zná Boost.Asio, musí mu být zřejmé, jak jsem se snažil zachovat styl práce s objektem Work.
Ve stručnosti a aniž byste viděli kód v IoServiceScheduleru:

  1. Když vytvoříte objekt Work, předáte mu odkaz na IoServiceScheduler, jehož později volaná metoda Run nemá skončit.
  2. Objekt Work notifikuje IoServiceScheduler o své existenci tím, že volá internal metodu AddWork scheduleru.
  3. IoServiceScheduler si poznamená, že existuje nový objekt Work. K tomu využije proměnné, které jsme viděli v jeho konstruktoru. Také si IoServiceScheduler u objektu Work ihned předplatí přes metodu RegisterWorkDisposedHandler informaci o tom, že byl objekt Work zničen. Zničením míníme vyvolání metody Dispose.
  4. IoServiceScheduleru dokáže pracovat s neomezeným počtem objektů Work, i když platí, že k tomu, aby metoda Run IoServiceScheduleru nevrátila řízení po zpracování všech tásků, stačí, aby existoval jeden objekt Work. Přidání dalších a dalších objektů Work nemá již na činnost IoServiceScheduleru vliv.

Na to, jak přesně IoServiceScheduler spravuje objekty Work, se můžete podívat sami. My se teď soustředíme na metody Run, RunOne, Poll, PollOne, bez kterých by IoServiceScheduler byl jen skládkou depresivních tásků, které nebudou nikdy zpracovány.

Z předchozího popisu metod Run, RunOne, Poll, PollOne vyplynulo, že mají podobné odpovědnosti a liší se hlavně v tom, za jakých podmínek přestanou zpracovávat  tásky a vrátí  dočasně propůjčený thread.
Jako první si na paškál vezmeme metodu Run a podíváme na důležité testy, kterými musí metoda Run projít.
Každý test používá IoServiceScheduler, který vytvoříme takto.

private ProxyScheduler m_proxyScheduler;

private IoServiceScheduler m_scheduler;

protected override ITaskScheduler Scheduler

{

get

{

return m_scheduler;

}

}

public override void InitializeTest()

{

m_scheduler = new IoServiceScheduler();

m_proxyScheduler = new ProxyScheduler(m_scheduler);

base.InitializeTest();

}

public override void CleanupTest()

{

m_scheduler.Dispose();

m_proxyScheduler.Dispose();

base.CleanupTest();

}

Kdyby vás překvapilo, proč používáme i ProxyScheduler, znovu vás odkážu na první díl seriálu.
Ale nyní už opravdu pojďme k metodě Run.

První test ověří, že když nemáme žádné tásky ke zpracování, metoda Run ihned vrátí 0 – žádný tásk nebyl zpracován.

[TestMethod]

public void Run_When_Zero_Tasks_Added_Then_Returns_Zero()

{

var result = m_scheduler.Run();

Assert.AreEqual(0, result);

}

V dalších testech ověříme, že když předáme jeden tásk, tak metoda Run tento tásk vyřídí a vrátí hodnotu 1.

[TestMethod]

public void Run_When_One_Task_Added_Then_Returns_One()

{

const int NUMBER_OF_SCHEDULED_TASKS = 1;

m_scheduler.Dispatch(() =>

{

});

var result = m_scheduler.Run();

Assert.AreEqual(NUMBER_OF_SCHEDULED_TASKS, result);

}

[TestMethod]

public void Run_When_One_Task_Added_Then_Task_Is_Executed()

{

bool wasTaskCalled = false;

m_scheduler.Dispatch(() =>

{

wasTaskCalled = true;

});

m_scheduler.Run();

Assert.IsTrue(wasTaskCalled);

}

Také bychom měli ověřit, že když scheduler obsahuje více tásků, tak jsou všechny vyřízeny a metoda Run stále vrací správný počet vyřízených tásků.

[TestMethod]

public void Run_When_More_Tasks_Added_Then_All_Tasks_Are_Executed()

{

bool wasTask1Called = false;

bool wasTask2Called = false;

m_scheduler.Dispatch(() =>

{

wasTask1Called = true;

});

m_scheduler.Dispatch(() =>

{

wasTask2Called = true;

});

m_scheduler.Run();

Assert.IsTrue(wasTask1Called && wasTask2Called);

}

[TestMethod]

public void Run_When_Two_Tasks_Added_Then_Returns_Two()

{

const int NUMBER_OF_SCHEDULED_TASKS = 2;

Enumerable.Range(0, NUMBER_OF_SCHEDULED_TASKS)

.Select(_ => m_scheduler.Dispatch(() =>

{

})).ToArray();

var executedTasksCount = m_scheduler.Run();

Assert.AreEqual(NUMBER_OF_SCHEDULED_TASKS, executedTasksCount);

}

Napíšeme další testy, které otestují, že se metoda Run chová správně při použití objektu Work. První test ověří, že metoda Run zpracuje jeden tásk, a poté, co je na objektu Work zavolána metoda Dispose, tak metoda Run vrátí řízení a návratovou hodnotou je 1 - jeden vyřízený tásk. V metodě cancelWorkAfterTimeout si můžete všimnout, jak je objekt Work vytvářen. Nepříjemné na tomto testu je, že když bude v metodě Run chyba a metoda Run po odstranění objektu Work nevrátí řízení, test poběží tak dlouho, dokud nevyprší přidělený maximální časový interval pro provedení samotného testu.

[TestMethod]

public void Run_When_One_Task_Added_And_Cancel_Work_Then_Returns_One()

{

m_scheduler.Dispatch(() =>

{

});

cancelWorkAfterTimeout();

var result = m_scheduler.Run();

Assert.AreEqual(1, result);

}

private void cancelWorkAfterTimeout(int? sleepMs = null)

{

const int DEFAULT_SLEEP = 1000;

var sleepTime = sleepMs ?? DEFAULT_SLEEP;

var work = new Work(m_scheduler);

ThreadPool.QueueUserWorkItem(_ =>

{

Thread.Sleep(sleepTime);

work.Dispose();

});

}

Ověříme také, že metoda Run vrátí řízení po zrušení objektu Work, i když nezpracovala žádné tásky.

[TestMethod]

public void Run_When_Zero_Tasks_Added_And_Cancel_Work_Then_Returns_Zero()

{

cancelWorkAfterTimeout();

var result = m_scheduler.Run();

Assert.AreEqual(0, result);

}

Další testy kontrolují, že metoda Run vyřídí všechny tásky, poté vrátí řízení a v návratové hodnotě máme správný počet vyřízených tásků

[TestMethod]

public void Run_When_More_Tasks_Added_And_Cancel_Work_Then_All_Tasks_Are_Executed()

{

bool wasTask1Called = false;

bool wasTask2Called = false;

m_scheduler.Dispatch(() =>

{

wasTask1Called = true;

});

m_scheduler.Dispatch(() =>

{

wasTask2Called = true;

});

cancelWorkAfterTimeout();

m_scheduler.Run();

Assert.IsTrue(wasTask1Called && wasTask2Called);

}

[TestMethod]

public void Run_When_Two_Tasks_Added_And_Cancel_Work_Then_Returns_Two()

{

const int NUMBER_OF_SCHEDULED_TASKS = 2;

Enumerable.Range(0, NUMBER_OF_SCHEDULED_TASKS)

.Select(_ => m_scheduler.Dispatch(() =>

{

})).ToArray();

cancelWorkAfterTimeout();

var executedTasksCount = m_scheduler.Run();

Assert.AreEqual(NUMBER_OF_SCHEDULED_TASKS, executedTasksCount);

}

U paralelního/asynchronního kódu se občas nevyhneme  testům, které jsou nebezpečné, protože porušují některou z F.I.R.S.T zásad pro unit/integrační testy.

Další test ověřuje, že metoda Run nevrátí řízení, dokud nezruším objekt Work. Nebezpečný je proto, že objekt Work zruším po 3 sekundách a předpokládám, že doba, po kterou běží test, je delší než dvě sekundy. Testy, které pracují takto vágně s časovými intervaly, částečně porušují F a R akronymu F.I.R.S.T. Tento test určitě není rychlý (Fast), protože běží několik sekund, a také není zcela“opakovatelný” (“Repeatable”). Pragmaticky vzato je ale tento test - stejně jako některé další méně bezpečné testy v knihovně RStein.Async- velmi užitečný a dostatečně bezpečný, takže všem pohoršeným puristům se omlouvám a přeju jim jejich ideální svět. Ať hodí kamenem…Veselý obličej

[TestMethod]

public void Run_When_Work_Exists_And_Zero_Tasks_Then_Method_Does_Not_Return()

{

const int WORK_CANCEL_DELAY_MS = 3000;

const double RUN_MIN_DURATION_S = 2.0;

var time = StopWatchUtils.MeasureActionTime(() =>

{

cancelWorkAfterTimeout(WORK_CANCEL_DELAY_MS);

m_scheduler.Run();

});

Assert.IsTrue(time.TotalSeconds > RUN_MIN_DURATION_S);

}

I další test není zrovna “košer”. Ověřuje, že metoda Run vrátí řízení “ihned”, když neexistuje žádný tásk a objekt Work byl sice IoScheduleru předán, ale byl ještě před voláním metody Run zrušen. Pojem “ihned“ zde nabývá netradičního významu “řízení z metody Run musí být vráceno za méně než půl sekundy”.

[TestMethod]

public void Run_When_Work_Canceled_And_Zero_Tasks_Then_Method_Returns_Immediately()

{

const double RUN_MAX_DURATION_S = 0.5;

var work = new Work(m_scheduler);

work.Dispose();

var time = StopWatchUtils.MeasureActionTime(() => m_scheduler.Run());

Assert.IsTrue(time.TotalSeconds < RUN_MAX_DURATION_S);

}

Poslední test, na který se podíváme v tomto článku, je test, který ověřuje, že když je metoda Run současně volána z více threadů (v testu jsou použity tři thready), tak jsou všechny tásky vyřízeny a součet návratových hodnot metod Run, tedy celkový počet všech vyřízených tásků bez ohledu na to, v kterém threadu k vyřízení tásku došlo, je roven počtu tásků, který jsme do IoServiceScheduleru poslali.

[TestMethod]

public async Task Run_When_Called_From_Multiple_Threads_Then_All_Tasks_Executed()

{

const int NUMBER_OF_SCHEDULED_TASKS = 100;

const int DEFAULT_TASK_SLEEP = 100;

const int NUMBER_OF_WORKER_THREAD = 3;

var countDownEvent = new CountdownEvent(NUMBER_OF_WORKER_THREAD);

int executedTasks = 0;

var allTasks = Enumerable.Range(0, NUMBER_OF_SCHEDULED_TASKS).Select(_ => m_scheduler.Post(() => Thread.Sleep(DEFAULT_TASK_SLEEP))).ToArray();

Enumerable.Range(0, NUMBER_OF_WORKER_THREAD).Select(_ => ThreadPool.QueueUserWorkItem(__ =>

{

int tasksExecutedInThisThread = m_scheduler.Run();

Interlocked.Add(ref executedTasks, tasksExecutedInThisThread);

countDownEvent.Signal();

})).ToArray();

await Task.WhenAll(allTasks);

countDownEvent.Wait();

Assert.AreEqual(NUMBER_OF_SCHEDULED_TASKS, executedTasks);

}

Testů pro metodu  Run je více, protože je potřeba otestovat  hraniční případy, ale z ukázaných testů by mělo být zřejmé, jak metoda Run pracuje.

Podívejme se teď na kód v metodě Run.

public virtual int Run()

{

checkIfDisposed();

return runTasks(withWorkCancelToken());

}

private CancellationToken withWorkCancelToken()

{

lock (m_workLockObject)

{

return (existsWork()

? m_workCancelTokenSource.Token

: withoutCancelToken());

}

}

private CancellationToken withoutCancelToken()

{

return CancellationToken.None;

}

Metoda Run zavolá metodu runTasks, které předá CancelToken, jestliže existuje alespoň jeden objekt Work, abychom po zrušení objektu Work dále neblokovali metodou Run propůjčený thread. Jestliže objekt Work neexistuje, je předána konstanta CancellationToken.None = na zrušení objekt Work reagovat nebudeme a metoda Run vrátí řízení ihned poté, co vyřídí všechny tásky.

Metoda runTasks

private int runTasks(CancellationToken cancellationToken, int maxTasks = UNLIMITED_MAX_TASKS)

{

try

{

setCurrentThreadAsServiceAllFlags(maxTasks);

return runTasksCore(cancellationToken);

}

finally

{

resetThreadAsServiceAllFlags();

}

}

private void setCurrentThreadAsServiceAllFlags(int maxTasks)

{

resetThreadAsServiceAllFlags();

setThreadAsServiceFlag();

m_isServiceThreadFlags.Value.MaxOperationsAllowed = maxTasks;

}

private void setThreadAsServiceFlag()

{

m_isServiceThreadFlags.Value.IsServiceThread = true;

}

private void resetThreadAsServiceAllFlags()

{

m_isServiceThreadFlags.Value.ResetData();

}

Metoda runTasks kromě odkazu na CancelToken, který se má použít, má argument maxTasks. Ten udává, kolik tásků je možné nyní vyřídit. Výchozí hodnota “počet tásků není omezen” metodě Run vyhovuje.

Metoda runTasks si nejprve “přivlastní” aktuální thread.  “Přivlastněním” threadu mám na mysli to, že do threadově lokální proměnné m_isServiceThreadFlags si poznamenáme, že současný thread je nyní thread IoServiceScheduleru a že může být použit pro vyřizování tásků, a také si poznačíme, kolik tásků můžeme nyní vyřídit. Poté vyvoláme metodu runTasksCore.

V sekci finally metoda runTaks zaručí, že poté, co metoda runTasksCore doběhne, tak se vlastnictví threadu vzdáme a  v proměnné isServiceThreadFlags nastavíme voláním metody resetThreadAsServiceAllFlags() výchozí hodnoty.

V metodě runTasksCore  konečně zpracujeme existující tásky:

private int runTasksCore(CancellationToken cancellationToken)

{

bool searchForTask = true;

var usedCancellationToken = cancellationToken;

var serviceData = m_isServiceThreadFlags.Value;

while (searchForTask)

{

searchForTask = false;

m_stopCancelTokenSource.Token.ThrowIfCancellationRequested();

try

{

Task task;

if (!tryGetTask(usedCancellationToken, out task))

{

continue;

}

m_stopCancelTokenSource.Token.ThrowIfCancellationRequested();

searchForTask = TryExecuteTaskInline(task, true) && !tasksLimitReached();

m_stopCancelTokenSource.Token.ThrowIfCancellationRequested();

}

catch (OperationCanceledException e)

{

Trace.WriteLine(e);

if (m_stopCancelTokenSource.IsCancellationRequested)

{

break;

}

usedCancellationToken = CancellationToken.None;

searchForTask = !tasksLimitReached();

}

}

return serviceData.ExecutedOperationsCount;

}

private bool tryGetTask(CancellationToken cancellationToken, out Task task)

{

if (cancellationToken != CancellationToken.None)

{

return m_tasks.TryTake(out task, Timeout.Infinite, cancellationToken);

}

return m_tasks.TryTake(out task);

}

private bool tasksLimitReached()

{

var serviceData = m_isServiceThreadFlags.Value;

if ((serviceData.MaxOperationsAllowed == UNLIMITED_MAX_TASKS) ||

(serviceData.ExecutedOperationsCount < serviceData.MaxOperationsAllowed))

{

return false;

}

return true;

}

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)

{

checkIfDisposed();

if (!isInServiceThread())

{

return false;

}

if (tasksLimitReached())

{

return false;

}

bool taskExecutedNow = false;

try

{

m_isServiceThreadFlags.Value.ExecutedOperationsCount++;

taskExecutedNow = task.RunOnProxyScheduler();

}

finally

{

if (!taskExecutedNow)

{

m_isServiceThreadFlags.Value.ExecutedOperationsCount--;

}

}

return taskExecutedNow;

}

Metoda runTasksCore  používá předaný cancellationToken a do proměnné serviceData si uloží odkaz na m_isServiceThreadFlags, protože  potřebujeme vědět, kolik tásků jsme již zpracovali a kontrolovat, jestli jsme nepřekročili maximální počet tásků. Cyklus while běží do té doby, dokud máme hledat a zpracovávat další tásk. Proměnná searchForTask je inicializována na true, takže se vnoříme do cyklu, ve kterém ihned proměnnou searchForTask nastavíme na false, protože nevíme, jestli další tásky existují.

Metoda tryGetTask se pokusí vrátit další tásk. Všimněte si, že na kolekci m_tasks zavoláme metodu m_tasks.TryTake(out task, Timeout.Infinite, cancellationToken), která vrátí stávající tásk v kolekci, nebo zablokuje thread do té doby, dokud nebude do kolekce tásk přidán anebo dokud nebude stornován cancellationToken. Tato varianta metody TryTake se použije, když existuje objekt Work a cancellationToken tedy nemá hodnotu CancellationToken.None. Jestliže objekt Work neexistuje, použijeme na kolekci metodu m_tasks.TryTake(out task), která buď ihned vrátí tásk, nebo zjistí, že kolekce je prázdná a proměnnou tásk nastaví na hodnotu null. K blokaci threadu ale nikdy nedojde.

Vraťme se do metody runTasksCore. Jestliže nebyl tásk nalezen, vrátíme se na začátek cyklu while - proměnná searchForTask má hodnotu false, a proto cyklus while i metoda runTasksCore skončí. Když je tásk nalezen, pokusíme se ho pomocí metody TryExecuteTaskInline spustit. Metoda TryExecuteTaskInline vždy ověří, že jsme v threadu, který nyní patří IoServiceScheduleru, a zkontroluje, že jsme nepřekročili maximální počet tásků, které můžeme v tomto vlákně vyřídit. Možná se divíte, proč kontroluju, že jsme v threadu IoServiceScheduleru, když jsme dříve tento příznak nastavili. Nezapomeňte, že metodu TryExecuteTaskInline používá i TPL a že může být vyvolána při vytváření tásku, kdy žádný thread “nevlastníme”.
Metoda TryExecuteTaskInline se pokusí přes ProxyScheduler tásk spustit. Jestliže byl tásk vykonán, tak na threadově lokální proměnné m_isServiceThreadFlags inkrementuje počet již zpracovaných tásků.

Metoda runTasksCore dovolí zpracovat další tásk jen tehdy, jestliže metoda TryExecuteTaskInline tásk úspěšně spustila a současně nebylo dosaženo maximálního počtu tásků, které lze zpracovat.
searchForTask = TryExecuteTaskInline(task, true) && !tasksLimitReached();

Všimněte si, že v metodě runTasksCore na několika místech kontrolujeme, jestli nemáme ukončit zpracování tásků, protože byla volána metoda Dispose IoServiceScheduleru.

m_stopCancelTokenSource.Token.ThrowIfCancellationRequested();

Tato opakovaná kontrola může být drahá a měli bychom pomocí výkonnostních testů zjistit, jestli si tolik kontrol můžeme dovolit. Frekvence kontroly stavu CancelTokenu by měla být kompromisem mezi tím, že zareagujeme v našem kódu na stornování operace velmi rychle, ale současně neplatíme za toto časté monitorování příliš velké výkonnostní penále. Tipnul bych si, že na tomto místě  jsou v IoServiceScheduleru ještě výkonnostní rezervy, ale jasno budeme mít  až po spuštění profileru. V této fázi se snažíme hlavně o to, aby chování IoServiceScheduleru bylo v souladu se zadáním. Pustit profiler můžeme kdykoli později.

Sekce catch v runTasksCore reaguje na stornování CancelTokenu, resp. CancelTokenů. Připomeňme si ji.

....

catch (OperationCanceledException e)

{

Trace.WriteLine(e);

if (m_stopCancelTokenSource.IsCancellationRequested)

{

break;

}

usedCancellationToken = CancellationToken.None;

searchForTask = !tasksLimitReached();

}

}

....

Jestliže byl stornován m_m_stopCancelTokenSource.Token,  k čemuž dojde po volání metody Dispose, tak přes klíčové slovo break rychle ukončíme další zpracování tásků. Když je ale výjimka OperationCanceledException vyvolána pro CancelToken, který jsme dostali jako argument metody  - a u metody Run víme, že CancelToken předaný do metody runTaksCore reprezentuje to, že existuje alespoň jeden objekt Work – pokračujeme ve zpracování tásků, jen předtím nastavíme CancelToken používaný pro získání tásku z kolekce m_tasks na hodnotu CancellationToken.None, protože bez existence objektu Work už nemáme právo blokovat propůjčené vlákno v metodě tryGetTask.

Metoda runTaksCore po dokončení cyklu while vrátí počet zpracovaných tásků.

return serviceData.ExecutedOperationsCount;

Stejnou hodnotu vrátí svým klientům i veřejná metoda Run, ze které jsme vyšli.

A tady je odměna. I když vám může být z toho pitvání vnitřností metody Run špatně, nejste v tom sami, a i všechny testy pro metodu Run mají zelenou barvu. Veselý obličej

image

Odměnou nám ale spíš bude to, že metody RunOne, Poll a PollOne můžeme napsat s využitím metody runTasksCore.

U metody RunOne víme, že bez ohledu na počet tásků čekajících na vyřízení, musí vždy vyřídit maximálně jeden tásk. Zde je ukázka několika testů.

[TestMethod]

public void RunOne_When_More_Tasks_Added_Then_Only_First_Task_Is_Executed()

{

bool wasTask1Called = false;

bool wasTask2Called = false;

m_scheduler.Dispatch(() =>

{

wasTask1Called = true;

});

m_scheduler.Dispatch(() =>

{

wasTask2Called = true;

});

m_scheduler.RunOne();

Assert.IsTrue(wasTask1Called && !wasTask2Called);

}

[TestMethod]

public void RunOne_When_Two_Tasks_Added_Then_Returns_One()

{

const int NUMBER_OF_SCHEDULED_TASKS = 2;

const int NUMBER_OF_RUNNED_TASKS = 1;

Enumerable.Range(0, NUMBER_OF_SCHEDULED_TASKS)

.Select(_ => m_scheduler.Dispatch(() =>

{

})).ToArray();

var executedTasksCount = m_scheduler.RunOne();

Assert.AreEqual(NUMBER_OF_RUNNED_TASKS, executedTasksCount);

}

[TestMethod]

public void RunOne_When_More_Tasks_Added_And_Cancel_Work_Then_Only_First_Task_Is_Executed()

{

bool wasTask1Called = false;

bool wasTask2Called = false;

m_scheduler.Dispatch(() =>

{

wasTask1Called = true;

});

m_scheduler.Dispatch(() =>

{

wasTask2Called = true;

});

cancelWorkAfterTimeout();

m_scheduler.RunOne();

Assert.IsTrue(wasTask1Called && !wasTask2Called);

}

[TestMethod]

public void RunOne_When_Two_Tasks_Added_And_Cancel_Work_Then_Returns_One()

{

const int NUMBER_OF_SCHEDULED_TASKS = 2;

const int RUNNED_TASKS = 1;

Enumerable.Range(0, NUMBER_OF_SCHEDULED_TASKS)

.Select(_ => m_scheduler.Dispatch(() =>

{

})).ToArray();

cancelWorkAfterTimeout();

var executedTasksCount = m_scheduler.RunOne();

Assert.AreEqual(RUNNED_TASKS, executedTasksCount);

}

Také bychom měli ověřit pomocí dalších "ne-zcela-bezpečných" testů, že metoda RunOne nevrátí řízení do té doby, dokud nevyřídí alespoň jeden tásk.

//Unsafe test

[TestMethod]

public void RunOne_When_Zero_Tasks_Then_Method_Does_Not_Return()

{

const int SCHEDULE_WORK_AFTER_MS = 3000;

const double RUN_MIN_DURATION_S = 2.0;

var time = StopWatchUtils.MeasureActionTime(() =>

{

scheduleTaskAfterDelay(SCHEDULE_WORK_AFTER_MS);

m_scheduler.RunOne();

});

Assert.IsTrue(time.TotalSeconds > RUN_MIN_DURATION_S);

}

//Unsafe test

[TestMethod]

public void RunOne_When_Work_Canceled_And_Zero_Tasks_Then_Method_Does_Not_Return()

{

const int SCHEDULE_WORK_AFTER_MS = 3000;

const double RUN_MIN_DURATION_S = 2.0;

var work = new Work(m_scheduler);

work.Dispose();

var time = StopWatchUtils.MeasureActionTime(() =>

{

scheduleTaskAfterDelay(SCHEDULE_WORK_AFTER_MS);

m_scheduler.RunOne();

}

);

Assert.IsTrue(time.TotalSeconds > RUN_MIN_DURATION_S);

}

//Unsafe test

Myslím, že princip činnosti metody RunOne je zřejmý, takže můžeme přistoupit k napsání metody RunOne.

public const int POLLONE_RUNONE_MAX_TASKS = 1;

public virtual int RunOne()

{

checkIfDisposed();

return runTasks(withGlobalCancelToken(), POLLONE_RUNONE_MAX_TASKS);

}

private CancellationToken withGlobalCancelToken()

{

return m_stopCancelTokenSource.Token;

}

To je skutečně vše. Zavoláme metodu runTasks, předáme jí m_stopCancelTokenSource.Token, který se bude používat při vyzvedávání tásků z kolekce m_tasks, a omezíme počet vyřízených tásků na jeden. Když metodu RunOne spustíte v době, kdy IoServiceScheduler žádné tásky neobsahuje, pak počká buď na to, až bude tásk do scheduleru přidán, nebo až metoda Dispose stornuje m_stopCancelTokenSource.Token. Jestliže je v IoServiceScheduleru po zavolání metody RunOne alespoň jeden tásk k vyřízení, metoda jej zpracuje a vrátí řízení.

Testy pro metody Poll a PollOne si můžete projít sami.
Metoda Poll pracuje podobně jako metoda Run, jen  nikdy nezablokuje stávající thread a po vyřízení všech čekajících tásků ihned svou činnost ukončí.

public virtual int Poll()

{

checkIfDisposed();

return runTasks(withoutCancelToken());

}

Metodě runTasks nepředáme CancelToken, takže vyzvednutí tásku z kolekce m_tasks nebude blokující. Připomenu, že výchozí hodnota druhého argumentu maxTasks metody runTasks je "počet není omezen".

Metoda PollOne stejně jako metoda RunOne vyřídí maximálně jeden tásk, ale pokud žádný tásk v IoServiceScheduleru není, tak nikdy neblokuje thread a ihned vrátí řízení.

public virtual int PollOne()

{

checkIfDisposed();

return runTasks(withoutCancelToken(), maxTasks: POLLONE_RUNONE_MAX_TASKS);

}

Komentář  k metodě PollOne už asi není třeba. Pokud tápete, doporučuju si projít testy pro metodu PollOne.

IoServiceScheduler je funkční, ještě nám zbývá vytvořit metody Dispatch, Post a Wrap z rozhrani IAsioTaskService.

Metoda Dispatch.

public virtual Task Dispatch(Action action)

{

checkIfDisposed();

if (action == null)

{

throw new ArgumentNullException("action");

}

var task = Task.Factory.StartNew(action,

CancellationToken.None,

TaskCreationOptions.None,

ProxyScheduler.AsTplScheduler());

return task;

}

public virtual Task Dispatch(Func<Task> function)

{

checkIfDisposed();

if (function == null)

{

throw new ArgumentNullException("function");

}

var task = Task.Factory.StartNew(function,

CancellationToken.None,

TaskCreationOptions.None,

ProxyScheduler.AsTplScheduler()).Unwrap();

return task;

}

public override void QueueTask(Task task)

{

checkIfDisposed();

m_tasks.Add(task);

}

private bool isInServiceThread()

{

return m_isServiceThreadFlags.Value.IsServiceThread;

}

Metoda Dispatch vytvoří z předaného delegáta Task pomocí TaskFactory z TPL a jako cílový scheduler předá vlastní ProxyScheduler, takže TPL nakonec použije metody TryExecuteTaskInline a QueueTask z našeho IoServiceScheduleru.

Jak jsem psal výše, metoda Dispatch může spustit delegáta  ihned v aktuálním threadu (“inline”), jestliže je sama zavolána v  threadu, který je nyní propůjčen IoServiceScheduleru. Taková situace nastane vždy, když se tásk běžící v IoServiceScheduleru snaží přes metodu Dispatch do stejné instance IoServiceScheduleru přidat další tásk. Projdete-li si znovu kód metody TryExecuteTaskInline, který je v gistu výše v tomto článku, uvidíte, že metoda dovolí spuštění tásku “inline”, jestliže metoda isInServiceThread vrátí true.

Metoda Post stejně jako Dispatch vytvoří nový tásk, ale musí u ní platit, že nikdy nedovolí vykonání tásku "inline".

public virtual Task Post(Action action)

{

checkIfDisposed();

if (action == null)

{

throw new ArgumentNullException("action");

}

return postInner(() => Dispatch(action));

}

public virtual Task Post(Func<Task> function)

{

checkIfDisposed();

if (function == null)

{

throw new ArgumentNullException("function");

}

return postInner(() => Dispatch(function));

}

private Task postInner(Func<Task> dispatcher)

{

bool oldIsInServiceThread = m_isServiceThreadFlags.Value.IsServiceThread;

try

{

clearCurrentThreadAsServiceFlag();

return dispatcher();

}

finally

{

m_isServiceThreadFlags.Value.IsServiceThread = oldIsInServiceThread;

}

}

Metoda Post používá metodu Dispatch, ale v metodě postInner ještě před voláním metody Dispatch vždy dočasně odstraníme u současného threadu příznak, že jde o thread vlastněný IoServiceSchedulerem (clearCurrentThreadAsServiceFlag()), a proto metoda TryExecuteTaskInline nepovolí vykonání delegáta "inline".

Metody Wrap slouží k vytvoření delegáta, pomocí kterého vytvoříte později tásk v IoServiceScheduleru, na kterém byla metoda Wrap volána. Kromě metod Wrap, které vracejí delegáta Action, jsem do IoServiceScheduleru přidal metody WrapAsTask, které vracejí delegáta Func<Task> a dovolují tak nejen zařadit nový tásk ke zpracování, ale také počkat na dokončení tásku. Metody Wrap i WrapAsTask používají již popsanou metodu Dispatch.

public virtual Action Wrap(Action action)

{

checkIfDisposed();

if (action == null)

{

throw new ArgumentNullException("action");

}

return () => Dispatch(action);

}

public virtual Action Wrap(Func<Task> function)

{

checkIfDisposed();

if (function == null)

{

throw new ArgumentNullException("function");

}

return () => Dispatch(function);

}

public virtual Func<Task> WrapAsTask(Action action)

{

checkIfDisposed();

if (action == null)

{

throw new ArgumentNullException("action");

}

return () => Dispatch(action);

}

public virtual Func<Task> WrapAsTask(Func<Task> function)

{

checkIfDisposed();

if (function == null)

{

throw new ArgumentNullException("function");

}

return () => Dispatch(function);

}

Jestliže by vám nebylo jasné, jak metoda Wrap pracuje, nezbývá mi, než znovu podotknout, že se můžete podívat na testy metody Wrap v IoServiceScheduleru.
Vím, že tento díl byl hodně dlouhý a únavný (na počátku jste byli varováni! Mrkající veselý obličej), ale ještě horší mi přišlo rozdělit povídání o IoServiceScheduleru do několika dílů.  Jak jsem sliboval v úvodu, další díly by už měly být stravitelnější.

Už příště se podíváme, jak můžeme využít IoServiceScheduler při psaní “coroutines”, a v článku  s pořadovým číslem čtyři také zjistíme, že napsat ThreadPoolScheduler, který používá na těžkou práci  IoServiceScheduler, je triviální problém na pár řádků. U ThreadPoolScheduleru také poznáme, jak se dají IoServiceScheduleru propůjčit některé thready na delší dobu. A ani potom s IoServiceSchedulerem ještě neskončíme, protože se nám bude hodit i při řešení problémů v dalších dílech seriálu.



Tuesday, 03 June 2014 11:02:00 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C#


 Monday, 26 May 2014
Task Parallel Library a RStein. Async 1 - Popis základních tříd a obcházení omezení v TPL

(Obnoveno ze zálohy, omlouvám se za formátování kódu)

V následující sérií článků chci představit některé konstrukce ze své knihovny RStein.Async. Většina popisovaných tříd intenzivně využívá a někdy i s gustem zneužívá Task Parallel library.  V článcích se tedy objeví i mnoho informací o samotné knihovně TPL a klíčových slovech async a await v C#.  V článcích předpokládám jen základní znalost TPL. Pod základní znalostí si představuju, že víte, jak spustíte nový Task,  k čemu se dá Task použít a jak získáte výsledek zpracování Tasku.
Knihovna RStein.Async vznikla jako vedlejší důsledek zkoumání možností Schedulerů v TPL, kdy jsem na projektech zkoušel, co si mohu s TPL dovolit a co je mi v TPL už odepřeno, nebo jsem zjišťoval, jaké je skutečně chování tříd, které jsou v dokumentaci nedostatečně popsány. Články jsou určeny i pro čtenáře, kteří třeba neví, proč by měli používat ConcurrentExclusiveSchedulerPair, protože v jednom díle popíšu nejen to, jaké jsou výhody tohoto scheduleru oproti běžně využívaným a hlavně zneužívaným synchronizačním primitivám (lock-Monitor, Mutex, SpinLock, Condition variable atd), ale napíšeme si i vlastní ConcurrentStrandSchedulerPair a pitváním jeho vnitřností zjistíme, jak se dá napsat ekvivalent třídy ConcurrentExclusiveSchedulerPair. Také chci ukázat, jak je možné napsat jednoduché aktory (a tím skutečně nemyslím ty směšné panďuláky na diagramu případů užití Veselý obličej )  s využitím našeho speciálního strand scheduleru a porovnám je s aktory, které lze napsat pomocí samotného TPL Dataflow v .Net Frameworku.
Pro lidi, kteří vyvíjejí v C++ a znají knihovnu BOOST ASIO, může být zajímavé, že se článku objeví názvy tříd, které důvěrně znají  - io_service a strand.  A dodám, že jsem nepoužil jen názvy, ale i odpovědnosti těchto tříd se shodují  s odpovědnostmi tříd v Boostu, i když jsou mé třídy pochopitelně napsány zcela jinak.
Snad se mi vás podařilo navnadit a pro nedočkavé dodám, že si mohou již dnes celou knihovnu stáhnout z Bitbucketu.

git clone git@bitbucket.org:renestein/rstein.async.git

Forky a pull requesty od kohokoli jsou skutečně vítány. Veselý obličej


Seriál  Task Parallel Library a RStein.Async  (předběžná osnova)

Task Parallel Library a RStein. Async 1 z n –  Popis základních tříd a obcházení omezení v TPL.

Task Parallel Library a RStein. Async 2 z n –  (boost) ASIO v .Net a IoServiceScheduler.

Task Parallel Library a RStein. Async 3 z n – Ukázky použití IoServiceScheduleru. Coroutines.

Task Parallel Library a RStein. Async 4 z n  – ThreadPoolScheduler založený na IoServiceScheduleru.

Task Parallel Library a RStein. Async 6 z n – Vytvoření StrandScheduleru.

Task Parallel Library a RStein. Async 7 z n – Náhrada za některé synchronizační promitivy – ConcurrentStrandSchedulerPair.

Task Parallel Library a RStein. Async 8 z n – Jednoduchý “threadless” actor model s využitím StrandScheduleru.

Task Parallel Library a RStein. Async 9 z n – Píšeme aktory I.

Task Parallel Library a RStein. Async 10 z n – Píšeme aktory II.

Task Parallel Library a RStein. Async 11 z n – Píšeme nový synchronizační kontext  - IOServiceSynchronizationContext.

Task Parallel Library a RStein. Async 12 z n – Použití IOServiceSynchronizationContextu v konzolové aplikaci a Windows službě.

(bude upřesněno)


Poznámka: V celé sérii článků budu používat slovo Task pro třídu, task pro název proměnné / argumentu metody a ”anglicismy” tásk/tásky místo “úloha/úlohy“ nebo jiného českého patvaru při zmínce o /úlohách-táscích/ v dalším textu. Předpokládám, že pro většinu vývojářů je takový text srozumitelnější.

V průběhu celého seriálu budeme psát nové schedulery. Jak asi víte, TaskScheduler je v TPL nízkoúrovňová třída, která je odpovědná za vyřízení předaných objektů Task. Každý potomek abstraktní třídy TaskScheduler rozhoduje o tom, kolik threadů se použije k vyřízení požadavků, i o tom, v jakém  pořadí  a kdy přesně budou předané objekty Task spuštěny. V .Net Frameworku jsou dva základní schedulery, které by měly pro většinu běžných scénářů postačovat. Scheduler, který je dostupný ve vlastnosti TaskScheduler.Default, využívá výchozí .Net ThreadPool a scheduler vrácený vlastností TaskScheduler.FromCurrentSynchronizationContext se hodí pro aplikace, ve kterých musí platit, že s ovládacími prvky na formuláři manipuluje jen tzv. UI thread, který ovládací prvek vytvořil (Windows Forms, WPF, Silverlight, Metro - Modern UI), jinak dojde k výjimce.

Chcete-li napsat vlastní TaskScheduler, podědíte z třídy TaskScheduler a přepíšete následující metody:

protected internal abstract void QueueTask(
	Task task
)

Metoda QueueTask většinou uloží předaný objekt task do nějaké své interní kolekce k pozdějšímu vyřízení.

protected abstract bool TryExecuteTaskInline(
	Task task,
	bool taskWasPreviouslyQueued
)

Metoda TryExecuteTaskInline je volána, jestliže infrastruktura TPL rozhodne, že objekt task by měl být spuštěn v aktuálním vlákně. Typicky je tato metoda volána, když čekáte na výsledek zpracování tasku (task.Wait) a thread, ve kterém je metoda Wait přímo či nepřímo zavolána, není blokován, ale využit infrastrukturou TPL ke zpracování tásku. Pravidelně zabíjený nebo i jen blokovaný thread skutečně není ve vícevláknových aplikacích dobrý thread. V druhém argumentu – taskWasPreviouslyQueued – máte příznak, který sděluje, jestli task již byl nebo nebyl předán metodě QueueTask a podle toho lze upravit logiku v Scheduleru. Jak uvidíme později u StrandScheduleru, tento příznak pro nás bude vemi důležitý proto, abychom dostáli všem zárukám při zpracování tásků, které StrandScheduler svým klientům poskytuje.
Jestliže se ve vlastním Scheduleru rozhodneme, že teď je možné task vyřídit, stačí zavolat metodu TryExecuteTask z bázové třídy TaskScheduler a ta se postará o veškeré další záležitosti včetně uložení výsledku zpracování nebo výjimky do objektu task.

Další metodu používá hlavně debugger, který dovede zobrazit frontu tásků čekajících na vyřízení v našem scheduleru.

protected abstract IEnumerable<Task> GetScheduledTasks()

Každý scheduler by také měl být schopen sdělit, kolik tásků dokáže v jednom okamžiku vyřizovat paralelně. Neboli jaký je nejvyšší stupeň konkurence v Scheduleru, což je údaj, který poskytneme zájemcům ve vlastnosti MaximumConcurrencyLevel.

public virtual int MaximumConcurrencyLevel { get; }

Na kód speciálních schedulerů se můžete podívat v Parallel Extension Extras od Microsoftu.

Když začnete psát méně tradiční schedulery, narazíte na jedno zásadní omezení. Nový objekt task je po pokusu o spuštění tásku (Task.Run, TaskFactory.Run, Task.Start) asociován s právě použitým schedulerem a nikdy už nemůže být předán jinému scheduleru. Když se o něco takového pokusíte, metoda TryExecuteTask vyhodí výjimku, ve které vám sdělí, že žonglování s táskem mezi schedulery není povoleno.
To asi nevypadá jako nějaké zásadní omezení, protože proč bychom měli vůbec chtít přehodit tásk z jednoho scheduleru do druhého? Jak uvidíte v dalších dílech serálu, napíšeme si postupně pro své schedulery dekorátory, kteří například zajistí, že po určitou dobu nebudou tásky zpracovávány, ale jen schraňovány v privátní frontě a teprve po splnění dalších podmínek uvolněny k vyřízení. Tedy tásk bude aktivován v nějakém jiném scheduleru, než je ten, který tásk později vyřídí.
Bez přepsání TPL bohužel nelze toto omezení, které by se dalo parafrázovat větou  “tásk předán scheduleru, z toho nutně a nepodmíněně plyne, že ten samý scheduler tásk také vyřídí”, jednoduše potlačit.

V knihovně RStein.Async jsem musel tedy zkusit navrhnout rozhraní a třídy tak, aby se “vlčí” knihovna TPL “nažrala” a přitom můj návrh (doufám, že ne “kozí”! Mrkající veselý obličej) zůstal celý.

Nejprve tedy musíme uspokojit TPL a přitom musíme být schopni zavolat metodu TryExecuteTask z třídy TaskScheduler odkudkoli z naší knihovny. TPL proto nabídnu speciální scheduler, který je z hlediska TPL plnohodnotným schedulerem. Tento scheduler nebude dělat nic jiného, než delegovat vykonání všech metod na mé vlastní “reálné” schedulery a čekat, až “reálný” scheduler požádá o vykonání Tasku

using System.Threading.Tasks;

namespace RStein.Async.Schedulers

{

public interface IProxyScheduler

{

bool DoTryExecuteTask(Task task);

TaskScheduler AsTplScheduler();

}

}

Rozhraní IProxyScheduler umí jen dvě věci. Metoda AsTplScheduler musí vrátit scheduler, se kterým umí pracovat TPL, a implementace metody DoTryExecuteTask zavolá metodu TryExecuteTask z TPL scheduleru.

Náš konkrétní proxy scheduler vypadá takto:

using System;

using System.Collections.Generic;

using System.Threading.Tasks;

namespace RStein.Async.Schedulers

{

public class ProxyScheduler : TaskScheduler, IProxyScheduler, IDisposable

{

private readonly ITaskScheduler m_realScheduler;

public ProxyScheduler(ITaskScheduler realScheduler)

{

if (realScheduler == null)

{

throw new ArgumentNullException("realScheduler");

}

m_realScheduler = realScheduler;

m_realScheduler.ProxyScheduler = this;

}

public override int MaximumConcurrencyLevel

{

get

{

return m_realScheduler.MaximumConcurrencyLevel;

}

}

public void Dispose()

{

Dispose(true);

}

public virtual bool DoTryExecuteTask(Task task)

{

if (task == null)

{

throw new ArgumentNullException("task");

}

bool taskExecuted = TryExecuteTask(task);

if (taskExecuted)

{

task.RemoveProxyScheduler();

}

return taskExecuted;

}

public virtual TaskScheduler AsTplScheduler()

{

return this;

}

protected override void QueueTask(Task task)

{

task.SetProxyScheduler(this);

m_realScheduler.QueueTask(task);

}

protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)

{

if (!taskWasPreviouslyQueued)

{

task.SetProxyScheduler(this);

}

return m_realScheduler.TryExecuteTaskInline(task, taskWasPreviouslyQueued);

}

protected override IEnumerable<Task> GetScheduledTasks()

{

return m_realScheduler.GetScheduledTasks();

}

protected void Dispose(bool disposing)

{

if (disposing)

{

m_realScheduler.Dispose();

}

}

}

}

V kódu asi není po přečtení předchozích odstavců moc překvapivých řádků. Tento scheduler bude vydán kdykoli, kde je očekáván Tpl scheduler, a proto:

1) Dědíme z abstraktní třídy TaskScheduler z TPL a podporujeme před chvílí popisované rozhraní IProxyScheduler.
2) Metoda AsTplScheduler vrátí odkaz na samotný objekt “this” – aktuální proxy scheduler.

3) Metody QueueTask, TryExecuteTaskInline, GetScheduledTasks a MaximumConcurrenyLevel jsou implementovány tak, že delegují na nějaký “reálný” scheduler z naší knihovny.

4) Metoda DoTryExecuteTask volá metodu TryExecuteTask.

Metody QueueTask a TryExecuteTaskInline také asociují a deasociují  ProxyScheduler s předaným táskem pomocí extenzních metod SetProxyScheduler a RemoveProxyScheduler.  Teď nás tolik trápit nemusí, jak jsem tyto metody napsal. Zájemci se ale mohou na kód podívat v předstihu.

Ještě jednou k terminologii schedulerů v knihovně RStein.Async, která může být zpočátku matoucí. ProxyScheduler má ve svém názvu slovo proxy, protože z pohledu všech dalších tříd v knihovně RStein.Async  tásky vyřizují jiné (“reálné”) schedulery, kteří nejsou, jak uvidíme za chvíli, potomkem třídy TaskScheduler z TPL a kteří čekají na to, až ProxyScheduler zavolá jejich metody. Při spuštění tásku musí ale i “reálný” scheduler požádat ProxyScheduler, aby metodou TryExecuteTask upozornil infrastrukturu TPL, že je třeba task nyní vyřídit.

Takže knihovna RStein.Async používá ProxyScheduler, který ale knihovna TPL vidí jako jediný  pro ni dostupný “reálný” scheduler.
Laboroval jsem s různými názvy pro ProxyScheduler, ale všechny další varianty mi přišly ještě horší.

Rozhraní ITaskScheduler je rozhraní, které podporují všechny "reálné" schedulery v knihovně RStein.Async.

using System;

using System.Collections.Generic;

using System.Threading.Tasks;

namespace RStein.Async.Schedulers

{

public interface ITaskScheduler : IDisposable

{

int MaximumConcurrencyLevel

{

get;

}

IProxyScheduler ProxyScheduler

{

get;

set;

}

Task Complete

{

get;

}

void QueueTask(Task task);

bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued);

IEnumerable<Task> GetScheduledTasks();

}

}

Toto rozhraní obsahuje všechny metody a vlastnosti, které jsme popisoval výše u schedulerů v TPL. Na tyto metody deleguje ProxyScheduler, jehož instance je z rozhraní ITaskScheduler také dostupná. V rozhraní naleznete také vlastnost Complete, která vrací Task, jenž by měl být ve stavu “dokončen” v okamžiku, když  scheduler již skončil svou práci a nemá být dále používán.

Schedulery v knihovně RStein.Async mají mnoho společných rysů, a proto jsem základní charakteristiky vytáhl do vlastní bázové třídy TaskSchedulerBase, aby všechny Schedulery nemusely reimplementovat celé rozhraní ITaskScheduler.

using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Threading;

using System.Threading.Tasks;

namespace RStein.Async.Schedulers

{

public abstract class TaskSchedulerBase : ITaskScheduler

{

private const string PROXY_SCHEDULER_ALREADY_SET_EXCEPTION_MESSAGE = "ProxyScheduler is already set and cannot be modified!";

private readonly CancellationTokenSource m_schedulerCancellationTokenSource;

private readonly TaskCompletionSource<object> m_serviceCompleteTcs;

private readonly object m_serviceLockObject;

private bool m_disposed;

private IProxyScheduler m_proxyScheduler;

protected TaskSchedulerBase()

{

m_disposed = false;

m_serviceLockObject = new Object();

m_serviceCompleteTcs = new TaskCompletionSource<object>();

m_schedulerCancellationTokenSource = new CancellationTokenSource();

}

protected object GetServiceLockObject

{

get

{

return m_serviceLockObject;

}

}

protected virtual CancellationToken SchedulerRunCanceledToken

{

get

{

return m_schedulerCancellationTokenSource.Token;

}

}

protected virtual CancellationTokenSource SchedulerRunCancellationTokenSource

{

get

{

return m_schedulerCancellationTokenSource;

}

}

public abstract int MaximumConcurrencyLevel

{

get;

}

public virtual IProxyScheduler ProxyScheduler

{

get

{

return m_proxyScheduler;

}

set

{

lock (GetServiceLockObject)

{

checkIfDisposed();

if (value == null)

{

throw new ArgumentNullException("value");

}

if (m_proxyScheduler != null)

{

throw new InvalidOperationException(PROXY_SCHEDULER_ALREADY_SET_EXCEPTION_MESSAGE);

}

m_proxyScheduler = value;

}

}

}

public virtual Task Complete

{

get

{

return m_serviceCompleteTcs.Task;

}

}

public void Dispose()

{

lock (m_serviceLockObject)

{

if (m_disposed)

{

return;

}

try

{

Dispose(true);

m_disposed = true;

m_serviceCompleteTcs.TrySetResult(null);

SchedulerRunCancellationTokenSource.Cancel();

}

catch (Exception ex)

{

Trace.WriteLine(ex);

m_serviceCompleteTcs.TrySetException(ex);

}

}

}

public abstract void QueueTask(Task task);

public abstract bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued);

public abstract IEnumerable<Task> GetScheduledTasks();

protected abstract void Dispose(bool disposing);

protected void checkIfDisposed()

{

if (m_disposed)

{

throw new ObjectDisposedException(GetType().FullName);

}

}

}

}

TaskSchedulerBase ponechá klíčové metody a vlastnosti abstraktní, protože zpracování tásků mohou řešit jen odvozené třídy, ale sama nabídne podporu pro ukončení činnosti scheduleru v mětodě Dispose .
O metodě Dispose v Schedulerech ještě budeme mluvit, protože deterministické ukončení činnosti scheduleru je pro některé scénáře klíčové, ale zde jen shrnu.
TaskSchedulerBase garantuje, že metoda Dispose bude volána jen jednou. Metoda Dispose  - jako jedno z mála míst v knihovně – používá kritickou sekci (lock). Metoda převede Task ve vlastnosti Complete do stavu “dokončen” bez ohledu na to, jestli chráněná metoda Dispose v odvozených třídách proběhne bez problémů, nebo jestli dojde k vyvolání výjimky, takže libovolný kód v aplikaci, který závisí na informaci, že nějaký scheduler dokončil svou činnost, může pokračovat, i když došlo k výjimce. Metoda Dispose také stornuje CancellationToken, aby i další kód v odvozených třídách mohl reagovat na ukončení činnosti scheduleru.

Základní rozhraní a třídy máme, obešli jsme i některá striktní omezení v TPL a je načase začít psát specializované schedulery. Dnes si ještě ukážeme jen primitivní CurrentThreadScheduler, u kterého pojmenování naznačuje, že všechny tásky budou vždy vykonány ihned a v aktuálním threadu. Již v dalším díle nás ale čeká zajímavý a užitečný IoServiceScheduler.

CurrentThreadScheduler je třída na pár řádků, ale už alespoň  nejde o abstraktní třídu,  a my si jejím napsáním ověříme, že naše stávající infrastruktura funguje.

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;

namespace RStein.Async.Schedulers

{

public class CurrentThreadScheduler : TaskSchedulerBase

{

private const int MAXIMUM_CONCURRENCY_LEVEL = 1;

public override int MaximumConcurrencyLevel

{

get

{

checkIfDisposed();

return MAXIMUM_CONCURRENCY_LEVEL;

}

}

public override void QueueTask(Task task)

{

checkIfDisposed();

ProxyScheduler.DoTryExecuteTask(task);

}

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)

{

checkIfDisposed();

ProxyScheduler.DoTryExecuteTask(task);

return true;

}

public override IEnumerable<Task> GetScheduledTasks()

{

checkIfDisposed();

return Enumerable.Empty<Task>();

}

protected override void Dispose(bool disposing)

{

}

}

}

Náš “reálný” scheduler s názvem CurrentThreadScheduler v metodách TryExecuteTaskInline a QueueTask spustí tásk s využitím ProxyScheduleru.
Metoda GetScheduledTasks vrátí prázdnou kolekci tásků, protože žádné tásky v metodě QueueTask neskladujeme.

I když jde o jednoduchý scheduler, měli bychom mít testy, které ověří, že se scheduler chová podle našich představ.

Nejprve CurrentThreadScheduler instanciujeme a předáme ho ProxyScheduleru.

protected override ITaskScheduler Scheduler

{

get

{

return m_scheduler;

}

}

protected override IProxyScheduler ProxyScheduler

{

get

{

return m_proxyScheduler;

}

}

public override void InitializeTest()

{

m_scheduler = new CurrentThreadScheduler();

m_proxyScheduler = new ProxyScheduler(m_scheduler);

base.InitializeTest();

}

......................

//Base tests

public TaskFactory TestTaskFactory

{

get

{

return m_testTaskFactory;

}

}

public override void InitializeTest()

{

m_testTaskFactory = new TaskFactory(ProxyScheduler.AsTplScheduler());

base.InitializeTest();

}

Metoda ProxyScheduler.GetTplScheduler() je využita k vytvoření instance TaskFactory z TPL, která nyní bude - nepřímo a aniž by si toho byla vědoma - používat ke spuštění tasků náš CurrentThreadScheduler.

A tady jsou testy:
Jestliže vytvoříme jeden tásk, tento tásk musí být vyřízen.

[TestMethod]

public async Task WithTaskFactory_When_One_Task_Is_Queued_Then_Task_is_Executed()

{

bool wasTaskExecuted = false;

await TestTaskFactory.StartNew(() => wasTaskExecuted = true);

Assert.IsTrue(wasTaskExecuted);

}

Když vytvoříme více tásků (v tomto testu jich je 8096), musí být všechny tásky vyřízeny.

[TestMethod]

public async Task WithTaskFactory_When_Tasks_Are_Queued_Then_All_Tasks_Are_Executed()

{

const int NUMBER_OF_TASKS = 8096;

int numberOfTasksExecuted = 0;

var tasks = Enumerable.Range(0, NUMBER_OF_TASKS)

.Select(_ => TestTaskFactory.StartNew(() => Interlocked.Increment(ref numberOfTasksExecuted))).ToArray();

await Task.WhenAll(tasks);

Assert.AreEqual(NUMBER_OF_TASKS, numberOfTasksExecuted);

}

Další testy ověřují charakteristiky, které by měl mít každý náš ITaskScheduler. Jedná se hlavně o ověření, že třída dodržuje doporučení  “odlehčeného” Dispose idiomu .

[TestMethod]

[ExpectedException(typeof (ObjectDisposedException))]

public void QueueTask_When_TaskScheduler_Disposed_Then_Throws_ObjectDisposedException()

{

var dummyTask = new Task(() => {});

Scheduler.Dispose();

Scheduler.QueueTask(dummyTask);

}

[TestMethod]

[ExpectedException(typeof (ObjectDisposedException))]

public void TryExecuteTaskInline_When_TaskScheduler_Disposed_Then_Throws_ObjectDisposedException()

{

var dummyTask = new Task(() => {});

Scheduler.Dispose();

Scheduler.TryExecuteTaskInline(dummyTask, false);

}

[TestMethod]

[ExpectedException(typeof (ObjectDisposedException))]

public void GetScheduledTasks_When_TaskScheduler_Disposed_Then_Throws_ObjectDisposedException()

{

Scheduler.Dispose();

Scheduler.GetScheduledTasks();

}

[TestMethod]

[ExpectedException(typeof (ObjectDisposedException))]

public void MaximumConcurrencyLevel_When_TaskScheduler_Disposed_Then_Throws_ObjectDisposedException()

{

Scheduler.Dispose();

var maximumConcurrencyLevel = Scheduler.MaximumConcurrencyLevel;

}

[TestMethod]

[ExpectedException(typeof (ObjectDisposedException))]

private void SetProxyScheduler__When_TaskScheduler_Disposed_Then_Throws_ObjectDisposedException()

{

Scheduler.Dispose();

Scheduler.ProxyScheduler = null;

}

[TestMethod]

[ExpectedException(typeof (ObjectDisposedException))]

private void GetProxyScheduler__When_TaskScheduler_Disposed_Then_Throws_ObjectDisposedException()

{

Scheduler.Dispose();

var proxyScheduler = Scheduler.ProxyScheduler;

}

[TestMethod]

public void Dispose_Repeated_Call_Does_Not_Throw()

{

Scheduler.Dispose();

Scheduler.Dispose();

}

[TestMethod]

public void Dispose_Does_Not_Throw()

{

Scheduler.Dispose();

}

}

A to je dnes skutečně vše.
V další části uvidíme nejen slibovaný IoServiceScheduler, ale ukážeme si, že IoServiceScheduler má speciální chování, které musí být pokryto mnohem robustnějšími testy.

Monday, 26 May 2014 09:51:00 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  


 Saturday, 23 March 2013
Záznam přednášky z MS Festu 2012 - Dependency injection v .NET bez pověr, iluzí a frikulínského nadšení

 

Pro ty z vás, kdo jste se mě ptali na záznam přednášky z MS Festu, mám (snad dobrou) zprávu. Záznam přednášky je od tohoto týdne dostupný na webu WUG.  O “snad dobré” zprávě píšu proto, že jsme sám nenašel odvahu se na sebe podívat.Smile

http://www.wug.cz/zaznamy/125-MS-Fest-2012-DI-v-NET-bez-pover-iluzi-a-frikulinskeho-nadseni

Materiály k přednášce.



Saturday, 23 March 2013 07:25:46 (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | C# | Návrhové vzory