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.
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.
- V konstruktoru vytvoříme scheduler a předáme ho instanci třídy Coroutine.
- 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.
- 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.
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, June 9, 2014 5:32:00 AM (Central Europe Standard Time, UTC+01:00)
C#