\


 Wednesday, February 13, 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, February 13, 2019 12:45:00 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C#


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


 Wednesday, June 25, 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, June 25, 2014 4:30:00 AM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C# | Návrhové vzory


 Monday, June 16, 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, June 16, 2014 6:06:00 AM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C#


 Monday, June 9, 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, June 9, 2014 5:32:00 AM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C#


 Tuesday, June 3, 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, June 3, 2014 11:02:00 AM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C#


 Saturday, March 23, 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, March 23, 2013 7:25:46 AM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | C# | Návrhové vzory


 Monday, December 3, 2012
Prezentace z přednášky na MS festu 2012 - DI v .NET bez pověr, iluzí a frikulínského nadšení

 

Tomáše Herceg & comp. opět po roce uspořádali další ročník konference MS Fest. A musím hned dodat, že z mého pohledu velmi povedený MS Fest, jehož organizace nikde neskřípala a na kterém jsme se cítil příjemně. Tímto organizátorům ještě jednou děkuju za skvělou organizaci konference a za veškerý servis, který poskytovali účastníkům konference i přednášejícím.

Na MS Festu jsme měl přednášku nazvanou Dependency injection v .Net Frameworku bez pověr, iluzí a frikulínského nadšení.
Sice jsem se jako každý rok po domluvě s organizátory na tématu přednášky dodatečně zděsil, že na přednášku mám jen 60 minut, a těsně před konferencí se stovkami účastníků jsme musel nahodit svůj přídavný a životní energii rychle spalující extrovertní pohon, ale samotná přednáška probíhala oproti minulému roku poklidně. Nemyslete si, já  teprve po minulém ročníku MS Festu dovedu ocenit, jaké je to blaho,  když s vámi v půlce probíhající  přednášky nezačne zuřivě diskutovat oponent z Nokie. Smile

Nabízel jsem tyto přednášky.

Tomáš Herceg mi původně v programu navrhl dvě přednášky, ale já jsme měl čas jen na přípravu jedné přednášky a vybral jsem tu, která dostala nejvíce hlasů.

Nevím, jaká je poptávka po pokročilejších/hard core přednáškách. Jak jsem psal na Twitteru, sám bych raději přednášel o “Task parallel library  pro pokročilé”, ještě raději o skrytých pokladech v RX Frameworku, ale RX si již dříve zamluvil Jarda Jírava. Bavilo by mě také přednášet o specialitkách typu dynamic, mohli bychom se pobavit o klíčových slovech async/await v netradičních kontextech, nebo bychom mohli napsat dalšího hostitele .Net Frameworku. To je alespoň malý výběr z témat, která mi jsou blízká, protože jsem podobné  vývojářské specialitky řešil pro různé firmy u nás i v zahraničí. Nevím, jaká by ale byla po těchto tématech na MS Festu poptávka, protože povídat si v potemnělé posluchárně jen pro sebe nebo prezentovat pro maximálně deset dalších  lidí má své kouzlo, ale - při vší úctě - intimní atmosféru mám raději s jinými než vývojářskými  kulisami a aktéry. Smile

Slíbená prezentace:

Doprovodný kód je na Bitbucketu:

Díky za to, že jste na mou přednášku přišli. A děkuju za hodnocení přednášky, moc jste mě potěšili. Smile

 

P .S. Ještě málá terminologická poznámka, proč používám slovo “kontajner”, a ne kontejner, na což se mě ptal už Tomáš Herceg po zaslání anotace.

Oficiálně [myšleno  - slovo kontajner žádný speciální význam] nemá – dokonce myslím ÚJČ slovo kontajner ani neuznává.

Viděl jsem, že se ale v ČR slovo kontajner docela vžilo a snažím se jeho použitím odlišit od konotací „kontejneru“ – u kterého mnoho lidí vidí spojitost s odpadky.:)

P. P. S. A ještě děkuju Alešovi Roubíčkovi za to, že si ochotně pročetl mou prezentaci a upozornil mě na místa, která by si zasloužila nějaké upřesnění.

P.P.P.S. Mrzí mě jediná věc. Na svých přednáškách většinou nezvětšuju písmo, na této přednášce jsem písmo ve Visual Studiu zvětšil přesně tak, jako to měl Tomáš Herceg na první přednášce, a při výkladu mě dost mátlo, že na obrazovce je vidět málo kódu, i když jsem měl z domova vyzkoušeno, že by kód měl být vidět bez problémů. Místo toho,  abych VS přepnul na celou obrazovku, tak jsem skroloval a v duchu se divil, proč je toho vidět tak málo. Takové momentální okno přednášejícího, který zapomněl, co udělal s fontem o přestávce. A ještě – poté, co mi spadl mikrofon a já si jej znovu nasadil, tak prý bylo slyšet hlavně v zadních řadách praskání a jiné pazvuky. Já jsem bohužel nic neslyšel a nikdo z posluchačů neprotestoval.



Monday, December 3, 2012 1:25:14 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | C# | Entity Framework | Návrhové vzory


 Friday, February 24, 2012
Entity Framework 4.3. Code First - (nechutný) problém s TPC mapováním?

 

Update 25. 2.2011: Tak chyba potvrzena EF týmem. Jedná se skutečně o chybu, která je částečně popsána v known issues.

Diego B Vega : @Rene Stein: Thanks for reporting this and for the repro. What you describe seems to be a bug in TPC mapping that we are already aware of and that we are planning to fix in the upcoming EF 4.3.1. Please take a look at the list of known issues above for more information.

Jedná se tedy o chybu, kterou někdo zmínil i  v komentářích. Zarážející ale je, že k chybě “chybí sloupec v databázi” se dostanete teprve tehdy, kdy vygenerujete databázi s jiným než požadovaným schématem, odchytnete výjimku při dotazování a podíváte se na popisek vnořené výjimky. V “known issues” EF by spíš podle mě mělo být  - ve verzi 4.3 se vám ani nepodaří vygenerovat databázi s TPC mapováním dědičnosti a volání metody MapInheritedProperties  při konfiguraci entit je jen zbytečná dekorace v kódu a cvičení v marnosti.

Mohl by prosím někdo ověřit, že jsem buď udělal nějakou triviální chybu při mapování, anebo potvrdit mé podezření, že je EF Code First v poslední verzi 4.3 natolik prolezlý chybami,  že v něm nefunguje ani tento triviální scénář.

Problém se snažím reprodukovat na tomto kódu.

Mám třídy Base a Derived. Jejich role asi vysvětlovat nikomu nemusím.Smile

Snažím se pro mapování třídy Derived do databáze použít v db kontextu strategii TPC – table per (concrete) class (metoda MapInheritedProperties). 

Po spuštění se aplikace vytvoří databáze se dvěma tabulkami. Struktura databáze ale odpovídá TPT strategii pro mapování dědičnosti:

Tabulka Base má sloupce Id a BaseProperty, tabulka Derived Id a Note. Volání MapInheritedProperties je tedy zcela ignorováno.

 

EFTables

Jak popisuju i v kódu, matoucí je to, že Entity Framework sice mapuje třídy do databáze podle TPT strategie, ale dotazy klade, jako kdyby v databázi byly třída Derived namapována TPC strategií.
Vygenerovaný SQL dotaz do tabulky Derived vypadá takto:

SELECT '0X0X' AS [C1], [Extent1].[id] AS [id], [Extent1].[BaseProperty] AS [BaseProperty], [Extent1].[Note] AS [Note] FROM [dbo].[Derived] AS [Extent1]

Schizofrenní Entity Framework se beze všech skrupulí snaží dohledat v tabulce Derived sloupec BaseProperty (TPC mapování), což pochopitelně skončí výjimkou při vykonávání dotazu, protože se jiná část jeho vícečetné osobnosti složené z nespolupracujících spoluautorů EF rozhodla při generování databáze, že TPT je pro každého aplikačního vývojáře vždycky jasná volba.

A protože perverzních projevů EF se při pátečním večeru nelze nabažit, tak tady je skript pro založení databáze, který jsem vytáhl z podkladového ObjectContextu a který by měl mapovat podle TPC, což se ale nestane, protože je proti databázi spuštěn skript zcela jiný.

Výsledek Trace.WriteLine(((IObjectContextAdapter) context).ObjectContext.CreateDatabaseScript());

Projekt s reprodukcí problému ke stažení.



Friday, February 24, 2012 10:06:21 PM (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | C# | Entity Framework | LINQ


 Tuesday, December 6, 2011
Demo z MS Festu

Několik lidí mi psalo na email, že by chtěli demo z první přednášky na MS Festu.

Zde je:

http://jdem.cz/sxs63

Podrobné informace, na které nebyl na přednášce čas, naleznete v sérii článků Tipy pro Windows Phone 7 aplikace v sekci o WP7 aplikacích.

Asi nemusím zdůrazňovat, že jde o demo, tedy kód nemusí mít produkční kvalitu, za nic neručím a určitě ke kódu není poskytována žádná podpora.Smile



Tuesday, December 6, 2011 3:17:03 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C# | Návrhové vzory | Silverlight | WP7


 Sunday, June 5, 2011
O špatně chápaném principu jedné odpovědnosti třídy (SRP) a o zneužívání myšlenek Domain driven designu (DDD)

 

Dnes na twitteru David Grudl odkázal na debatu, která se týká vlastností v PHP. O vlastnostech v PHP mluvit nechci, ale v tomto  příspěvku se chci dotknout některých “dogmat”, které se ozývají stále častěji a které byly použity jako univerzální kladivo na oponenty  i v odkazované diskuzi.

Jedno zvláštní dogma se týká principu jedné odpovědnosti třídy (Single responsibility principle). Tento princip říká, že třída by měla mít jednu přesně vymezenou odpovědnost, která je v souladu s jejím názvem. I když na první přečtění se tento princip zdá neproblematický, dá se zneužít jako univerzální kladivo. Dogmatici mi říkali, že jedna odpovědnost si vynucuje, aby třída vždy měla právě jednu metodu, která tuto odpovědnost realizuje. Není nad přehledný svět objektových dogmatiků, kde objekt je jen stupidní kontajner na jednu (de facto globální?) funkci.Smile

Dogmatiky tohoto zvláštního ražení  zanedbejme jako ztracené případy a SRP obohaťme o další vysvětlení, které říká, že třída by měla mít jen jeden důvod ke změně. Tento princip je užitečný v tom, se snaží z aplikací odstranit všemocné božské (God) objekty, které mnohdy už svým názvem signalizují, že řeší spoustu věcí. UniversalOrderAndInvoiceProcessor oznamuje, že se bude měnit nejen, když se změní zpracování objednávek, ale také když se změní zpracování faktur. Jednoduché, což? Proč o tomhle jednoduchém principu vůbec dále mluvit?

V diskuzi se o SRP mluví (viz i příspěvky níže), ale diskutující tam ve  své argumentaci používají něco, čemu na kurzech u SRP říkám falešné alternativy.

Mějme stejně jako v diskusi svou třídu Image, která nese informace o obrázku. Obrázek chceme uložit.

Varianta 1, kdy obrázek nese informace a současně nabízí metodu Save, ve které uloží data do souboru.

Co je v diskuzi vyčítáno této třídě? Porušuje princip jedné odpovědnosti, protože podle některých (Jiří Knesl, Ondřej Mirteš)  řeší dvě věci najednou – nese data o obrázku a současně data ukládá. Souhlasím, že jde o porušení SRP, ale hlavním důvodem je to, že metoda Save je napsána tak,  že třídu Image ukládáme vždy do souboru. Co když budeme chtít třídu Image uložit do nějakého “response” streamu na webovém serveru, nebo uložit přímo do databáze? Tuto třídu skutečně budeme měnit ze dvou důvodů – jednou, když přidáme nebo odebereme informace o obrázku a také, když budeme chtít ukládat obrázek do databáze, musíme rozšířit stávající metodu Save, což povede k tomu, že metoda bude mít v sobě nějaký podivný switch a  bude trpět smíšenou odpovědností, protože bude dělat několik věcí najednou,  nebo můžeme přidat novou samostatnou metodu SaveToDb.

Jedinou (!?) alternativou v diskuzi k tomuto postupu je vyvedení odpovědnosti za ukládání do různých úložišť do samostatných objektů, které mohou být  skryty za jednotným rozhraním.

Toto řešení důsledně separuje odpovědnosti, navíc je velmi snadné přidat další implementaci rozhraní IImagePersistor, např. DbPersistor, který data uloží do databáze. Už v diskuzi Jakub Vrána ale upozorňuje na to, že se mu nelíbí, jak se řešení komplikuje pro uživatele-vývojáře, který s třídami bude pracovat, protože tento vývojář musí vědět, že existuje nějaký IImagePersistor/FilePersistor odpovědný za uložení dat. Třída Image nestačí k tomu, abyste dokázali vygenerovat data obrázku a uložit je, což může být ve vaší knihovně častý scénář. Také bych rád poprvé v tomto článku připomněl princip OOP, ke kterému se za chvíli vrátím, že objekt představuje jednotu svého stavu a chování, které je pro tento stav definováno.

Psal jsem o falešných alternativách, můžeme najít i jiná řešení. Co ponechat metodu Save ve třídě Image, ale z třídy Image udělat tzv kompozitor - objekt, který skládá své chování tak, že využívá další pomocné objekty, na kterých závisí, a nabízí intuitivní rozhraní pro klienty.

Odpovědnosti jsou stále separovány a dokonce třída Image, náš kompozitor, dodržuje pravidlo, které říká, že kompozitor by měl být jednodušší než suma funkcí jeho pomocných objektů.  Klient třídy Image nemusí pracovat přímo s třídou FilePersistor, a přitom nemáme kód pro ukládání do souboru přímo ve třídě Image. Problém je, že metoda Save třídy Image vždy vytváří FilePersistor. Klient třídy Image si nemůže vyžádat to námi dříve zmiňované ukládání obrázku do databáze, a navíc třída Image závisí na jedné konkrétní třídě FilePersistor, u níž přímo volá konstruktor. V třídě Image mixujeme vytváření grafu spolupracujících objektů se samotným použitím pomocných objektů. Opět jde o dvě odpovědnosti, které bychom měli oddělit – SRP, nezapomeňme.

Nejprve ale zkusme vyřešit problém s tím, že klient nemůže ukládat data do databáze, protože třída Image ukládá data vždy do souboru.

Jednoduše přidáme další variantu metody, která přijímá odkaz na IImagePersistor, v našem případě třeba na DbPersistor. Původní metoda Save bez argumentů řeší ukládání do souboru. Ukládání do souboru je nejčastější scénář, který je zvolen jako výchozí. Stále ale tady máme problém s tím, že v metodě Save konstruujeme "natvrdo" FilePersistor. A navíc naše API klientům trochu lže. V podtextu klientovi sděluje, že výchozí metoda Save nemá žádné další závislosti, i když z implementace, !a jen z implementace!,  je zřejmé, že jsme závislí na přítomnosti třídy FilePersistor. Poznámka: V C# 4 můžeme použít volitelné argumenty u jedné metody, ale na principu této varianty řešení se moc nemění.

Zkusme naše prozatím ulhané API vylepšit a dodržet SRP.  Oddělme nyní konstrukci pomocných objektů, na kterých závisíme, od jejich použití v metodě Save.

Objekt Image si nyní v konstruktoru vynucuje předání IIMagePersitoru. Když klient IImagePersistor nepředá, objekt nezvznikne – sám konstruktor garantuje, že buď objekt Image má vyplněny všechny závislosti, nebo vůbec nevznikne. Vytvořili jsme konstruktor, který může použít a automaticky naplnit DI kontajner, nebo různé abstraktní továrny registrované v DI kontajneru  apod. DI kontajner je přesně tím objektem, který by měl být v aplikaci odpovědný za konstrukci grafu objektů, v metodě Save objektu Image injektovaný IImagePersistor jen používáme. SRP v praxi.

Možný že ale v tomto případě je injektování závislostí přes konstruktor moc striktní. Co když nám skutečně vyhovuje, že můžeme bez DI kontajneru vytvořit objekt Image, který bude data ukládat do souboru. Pak můžeme využít injektování přes vlastnosti, kdy příslušnou vlastnost po vzniku objektu vyplníme rozumnou výchozí hodnotou – v našem případě instancí FilePersistoru. Poté ale platí, že třídu Image stále částečně zatěžujete konstrukcí objektů…
U většiny DI kontajnerů je preferováno injektování závislostí přes konstruktory, všechny, které znám,  si ale poradí ale i s injektováním závislostí přes vlastností a u MEFu bych řekl, že injektování závislostí pomocí vlastností hrají prim.

Všechny tyto varianty mají své výhody a nevýhody a asi nemusím zdůrazňovat, že ani jedna není univerzálním kladivem. Varianty s injektováním závislostí (konstruktor, metoda, vlastnost) jsou samozřejmě mnohem lépe testovatelné.

Dokážu přidat i další příklady, ale chtěl jsem,  abyste viděli, že SRP není ani nesmysl, ale ani princip, který by, podobně jako to zaznělo v diskuzi, sděloval – existují jen dvě alternativy, jak rozdělovat odpovědnosti, a ZROVNA TA TVOJE JE ŠPATNĚ.

A poslední  poznámka:

Jiřé Knesl také v diskuzi uvedl: “ objekt buďto data reprezentuje (pak má settery/gettery), nebo vykonává činnost (pak dostane data parametry)”. Tohle je podle mě  postoj blízký hlavně některým Javistům, o čemž svědčí i podle mého soudu schematický a nevěrohodný článek, který se zabývá vlastnostmi v Javě a na který se J. Knesl odkazuje. Znovu připomínám, že objekt představuje jednotu svého stavu a chování, které je pro tento stav definováno.  Objekt, který má jen gettery a settery, je ”krabičkou na data”, pouhou strukturou známou i z neobjektových jazyků, a když má objekt jen metody, tak jde o (v mnoha případech skutečně globální) funkce/procedury, které prefixujeme názvem proměnné/třídy. V diskuzi to myslím nezaznělo, ale když někdo razí tuto drastickou separaci chování od samotných dat, často dodává, že takto je to přece definováno Evansem, tedy autoritou,  v kanonické knize o Domain Driven Designu. Když se ptám, kde o tom Evans mluví, dozvím se, že Evans má objekty, které mají svůj stav (vlastnosti)  a s objekty pracují speciální business-doménové služby (chování). I když mám vůči DDD spoustu výhrad, zde Evanse špatně interpretují – Evans by model, kde objekty mají jen stav a nemají žádné chování, nazval anemickým modelem – izolovaná data podepřená berličkami nesouvisejících globálních funkcí. Business služby jsou, zjednodušeně řečeno, určeny pro zapsání složitější business logiky, na které spolupracuje více objektů a žádný participující objekt není sám o sobě přirozeným kandidátem, do kterého by bylo vhodné logiku situovat.

Zde bych mohl pokračovat dále k rozdělení objektů v DDD, ke skutečnému významu vlastností u objektu, co říká princip “tell, don't ask”, ale už teď mi původně krátký komentář k SRP a DDD až moc nabobtnal. Když budete mít zájem o další naznačená témata, napište prosím komentář k článku.



Sunday, June 5, 2011 9:13:04 PM (Central Europe Standard Time, UTC+01:00)       
Comments [19]  Analytické drobky | C# | Návrhové vzory | UML


 Monday, March 21, 2011
Prezentace Moderní trendy ve vývoji aplikací

Přibližně před rokem jsem u dvou firem začínal sérii technologických kurzů subjektivním shrnutím změn (nejen) v aplikacích psaných v .Net Frameworku. Nedávno jsme ji s kolegou náhodou otevřeli a pobavili jsme se nad tím, jak je rok v IT stále dlouhá doba a že zde dvojnásobně platí “tempus fugit”. Napadlo mě, že se nad prezentací možná se pobaví i někdo další, hlavně v pasážích, kde jemně naznačuju zálibu Microsoftu v zařezávání technologií.Smile

U prezentace je třeba mít na paměti:

  1. Jedná se jen o osnovu “přehledové“ a cca dvouhodinové přednášky.

  2. Témata, typy projektů a technologie jsou v přednášce voleny podle zájmu zákazníka.

  3. Snažil jsem se nebýt hned  v této úvodní přednášce příliš ostrý a konfliktní.Smile

  4. Zvolená témata se týkala oblastí, které jsme v dalších dnech probíraly detailněji na konkrétních projektech vytvořených na návazných kurzech. Po pár zkušenostech si myslím, že jediný smyslupný kurz zabývající se technologií či programovacím jazykem je ten, na kterém píšete před účastníky kód. Tato přednáška byla koncipována jako motivační úvod k dalším tématům.


Monday, March 21, 2011 1:11:41 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | ASP.NET | C# | Compact .Net Framework | LINQ | RX Extensions | Silverlight | Web Services | Windows Forms | WP7


 Monday, January 10, 2011
Tipy pro Windows Phone 7 aplikace IV - intermezzo I - zjednodušená registrace serializovatelných tříd nesoucích tranzientní stav v KnownTypesDictionary

 

Dnešní článek je jen “intermezzem”, protože doplňuje předchozí článek o slíbenou informaci, jak můžeme automaticky registrovat serializovatelné třídy, jejichž instance  nesou tranzientní stav, který je uložen  v KnownTypesDictionary. Přechozí článek končil ukázkou, jak můžeme vrátit staticky definovaný seznam typů, dnes se poohlédneme po trochu dynamičtějším řešení. I když postup není omezen jen na WP7 aplikace, ale můžete ho použít v kterékoli aplikaci, která využívá (WCF) DataContractSerializer, my se zaměříme v článku opět hlavně na řešení WP7 specialitek.

Přečetli jste si první odstavec a nevíte, co je tranzientní stav? Hledáte marně v MSDN typ KnownTypesDictionary? Nedivte se, milostný akt sice můžete rozjet bez předehry, ale abyste rozuměli  tomuto intermezzu, pečlivě a pomalu si přečtěte jako předehru k intermezzu tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”, i díl třetí, kde se bavíme o KnownTypesDctionary.

Předehru máte za sebou, takže víte, že KnownTypesDictionary je specializovaný objekt Dictionary, ve kterém je klíčem řetězec a hodnotou libovolný objekt a že v objektech KnownTypesDictionary ukládáme tranzientní stav view modelů při každém “tombstoningu”. I když KnownTypesDictionary může jako hodnotu nést libovolný objekt, my musíme instruovat DataContractSerializer, které objekty a hlavně z kterých odvozených tříd má v objektu Dictionary očekávat.

Když do KnownTypesDictionary ukládáme v jedné aplikaci instance z tříd OrderVO a InvoiceVO a v jiné aplikaci instance TwitterPost a ObservableCollection<TwitterPost >, musíme vždy znovu zmíněné třídy registrovat pomocí atributu KnownType.

Pro připomenutí následuje výpis kódu, kterým jsme končili a který registruje serializovatelné třídy v aplikaci “natvrdo” pomocí statické metody GetKnownTypes, jejíž název je předán do konstruktoru atributu KnownType.

 

Napevno zadrátované typy v KnownTypesDctionary nám nemusí vadit v jedné aplikaci, ale když chceme používat KnownTypesDctionary v mnoha různých aplikacích, musím řešení upravit.

Místo abychom třídy v metodě GetKnownTypes registrovali přímo, delegujeme odpovědnost za vrácení serializovatelných typů na specializovaného poskytovatele.

BTW: To víte, že první a okouzlující zákon vztahu mezi třídami zní:  “Když je třída vyčerpána množstvím odpovědností, vždy si může přičarovat otroka, který špinavou práci udělá za ni”?. Občas se tomuto zákonu  také říká SRP – princip jedné odpovědnosti třídy”.Smile

Poskytovatel je objekt podporující rozhraní IKnownTypeProvider s jednou samopopisnou metodou.

Refaktorizujeme třídu KnownTypesDictionary, aby při vracení serializovatelných typů využívala objekt IKnownTypeProvider.

Do nové statické vlastnosti KnownTypeProvider je ve statickém konstruktoru přiřazen DefaultKnownTypesProvider, o kterém budeme mluvit za chvíli, ale do vlastnosti KnownTypeProvider můžete klidně vložit pro účely vaší aplikace lépe přizpůsobený IKnownTypeProvider.

Metoda GetKnownTypes vrátí všechny staticky registrované typy v poli KNOWN_TYPES společně s  typy, které dodá  IKnownTypeProvider. Typy jsou získány jen při prvním volání metody GetKnownTypes a jsou cachovány v proměnné _cachedKnownTypes, abychom opakovaným získáním typů z  IKnownTypeProvider zbytečně neplýtvali výkonem WP7 telefonů ani trpělivostí uživatele. Předpokladem tohoto přístupu samozřejmě je, že v aplikaci je fixní množina serializovatelných typů, která se za běhu aplikace již nemění. Jestli vám to nevyhovuje, odstraníte cache z KnownTypesDictionary za 10 sekund.

Jaké odpovědnosti bude mít DefaultKnownTypesProvider?

  1. DefaultKnownTypesProvider nám vrátí všechny deskriptory třídy (Type) označené atributem [DataContract] v hlavní assembly aplikace.

  2. DefaultKnownTypesProvider nám vrátí všechny deskriptory třídy (Type) označené atributem [DataContract] v dalších assembly, ze kterých je složena aplikace.

  3. DefaultKnownTypesProvider nalezne další a pro aktuální aplikaci specifické objekty podporující rozhraní IKnownTypeProvider a vrátí seznam všech typů z těchto providerů.

  4. Pro všechny nalezené typy T zaregistruje i kolekci Observable<T>. To znamená, že pro třídu OrderVO označenou atributem DataContract je automaticky vrácen z poskytovatele nejen její Type (typof(OrderVO), ale i deskriptor kolekce typeof(ObservableCollection<OrderVO>).

Kód třídy DefaultKnownTypesProvider:

V metodě GetKnownTypes vyzvedneme všechny typy z “hlavní”  (vstupní) assembly. Hlavní assembly získáme v metodě getExecutingAssembly přes vlastnosti třídy Deployment.

Typy z hlavní  assembly sloučíme s typy v dalších assembly voláním metody getRefencedAssembliesTypes(). Ve WP7 jsem bohužel nenašel způsob, jak seznam dalších assembly získat, a proto další assembly poskytují objekty IAssemblyTypesProvider nalezené v hlavní assembly.

Chcete-li tedy automaticky vyhledat typy označené atributem DataContract v další assembly, vložte do hlavní assembly třídu podporující rozhraní IAssemblyTypesProvider.

Do proměnné collectionTypes v metodě GetKnownTypes vygenerujeme pro všechny nalezené typy jejich kolekce. Možná trochu složitě vypadající kód jen zabraňuje tomu, abyste nalezené deskriptory tříd typu ObservableCollection<T> v proměnné types balili znovu do kolekce ObservableCollection. Jinými slovy, když v proměnné types bude kolekce ObservableCollection<OrderVO>, do proměnné collectionTypes nebude generována kolekce ObservableCollection<ObservableCollection<OrderVO>>.

Na konci vracíme nalezené typy sloučené s vygenerovanými typy kolekcí ObservableCollection<T> a dalšími a pro aplikaci specifickými typy z ostatnich poskytovatelů . Za poskytovatele považujeme další objekty IKnownTypeProvider nalezené v hlavní assembly.

A tím máme hotovo.

Připomínám, že DefaultKnownTypesProvider můžete využít nejen ve WP7 aplikacích, ale ve všech aplikacích v .Net Frameworku, kde je používán DataContractSerializer a vy chcete automaticky registrovat odvozené serializovatelné typy.

Příště již začneme stavět aplikaci založenou na našem "mini frameworku”, abyste viděli, k čemu tyto počáteční díly seriálu včetně intermezza, které právě čtete, vůbec byly.Smile

Předcházející články:

Tipy pro Windows Phone 7 aplikace I

Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”

Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)



Monday, January 10, 2011 1:37:12 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | C# | Návrhové vzory | Silverlight | WP7


 Friday, December 17, 2010
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”

 

Již v prvním dílu seriálu o vývoji WP7 aplikací jsem zmiňoval nejen to, že mobilní verze Silverlightu ve WP7 je založena na Silverlightu 3, ale také, že mobilní Silverlight má své unikátní rysy, které v žádné desktopové verzi Silverlightu nenalezneme. Jednou ze změn je životní cyklus aplikace, včetně tzv. tombstoningu. Termín ”tombstoning”, kterého se i  v tomto článku budu držet, abych nemusel zavádět nějaké směšně znějící české ekvivalenty, má asi naznačovat, že WP7 podporují nejen tradiční spuštění a ukončení aplikace, ale i jakýsi hybridní stav, v němž instance naší aplikace může být dočasně ukončena (“umrtvena”) například tím, že uživatel spustí jinou aplikaci, a poté může být z hlediska uživatele WP7 telefonu naše původní aplikace navrácena k životu,  a to dokonce ve stavu, v jakém ji předtím uživatel zanechal. O smysluplnosti “tombstoningu” mám své pochybnosti a raději bych ve WP7 viděl tradiční multitasking, ale vývojářův boj s chováním “zombie” aplikací ve stavu “tombstoningu” má také něco do sebe. V tomto článku bych rád posunul boj se “zombie-tombstonovanými” aplikacemi do dalšího levelu a nabídnul pár “cheatů” .Smile

Přechody mezi stavy WP7 aplikace nám nejlépe objasní  metody pro obsluhu životního cyklu aplikace, které jsou po založení projektu automaticky vygenerovány v souboru App.xaml.cs.

Metoda  Application_Launching je  provedena jednou po spuštění aplikace. Jde o metodu, ve které můžete načíst dříve uložená data z “isolated storage”.  Tato metoda NENÍ volána při  obnovení “tombstonované” aplikace.

Metoda Application_Closing je provedena jednou při ukončení aplikace. Jde o metodu, ve které typicky uložíte data do “isolated storage”. Jde o stejná data, která  načtete při dalším spuštění aplikace v již popsané metodě Application_Launching a můžeme tedy říci, že jde o data s delší záruční lhůtou, která mají význam pro různé instance aplikace. Když stáhnete z webové služby nějaké číselníky (typy zákazníků, kategorie objednávek) a nechcete je po startu aplikace stahovat vždy znovu, uložíte si je a při příštím spuštění aplikace rychleji naběhne, protože nemusí stahovat všechna data z webových služeb ihned po startu.

Další metody se týkají již tombstoningu. Metoda Application_Deactivated je volána vždy, když je vaše aplikace “tombstonována” a v této metodě byste měli uložit všechna data, která potřebujete k tomu, abyste mohli po návratu z “tombstonovaného” stavu aplikaci zobrazit uživateli tak, jako kdyby běžela celou dobu a k žádnému “tombstoningu” nedošlo. Pro vývojáře ale “tombstoning” ve skutečnosti znamená, že aktuálně běžící instance aplikace je zlikvidována! Můžeme tedy říci, že v této metodě hlavně ukládáme data s kratší záruční lhůtou, která mají význam jen pro další instanci aplikace po návratu z “tombstonovaného” stavu. Data, která chcete mít k dispozici i po obnovení z “tombstoningu” můžete ukládat pod vámi zvolenými identifikátory v objektu typu IDictionary ve vlastnosti PhoneApplicationService.Current.State. Data, která zde uložíte, musí být serializovatelná, protože se nedrží jen v paměti telefonu, ale jsou ukládána i do souboru.

Je ale nutné si uvědomit, že když je aplikace “tombstonována”, nemáte garantováno, že se uživatel do vaší aplikace vrátí a že budete obnoveni z “tombstonovaného” stavu. I v této metodě je proto vhodné ukládat data, která ukládáte v metodě Application_Closing popsané výše.

Uživatel může také spustit vaší aplikaci znovu, tedy spustit novou instanci aplikace, aniž by se vrátil k dříve “"tombstonované” aplikaci a vy svůj stav - data, o nichž jsem říkal, že mají kratší záruční lhůtu - dříve uložený v metodě Application_Deactivated nikdy nepoužijete. Když se uživatel vrátí do vaší aplikace, což většinou nastane po stisknutí tlačítko “Back”, kdy se v zásobníku dříve spuštěných aplikací, který je spravován přímé operačním systémem, stane aktivním vaše aplikace, musíte obnovit dříve uložený stav v metodě Application_Activated. Napsal jsem, že jde o zásobník aplikací, ale je potřeba si uvědomit, že jde spíš o metaforu, protože aplikace a jejich stránky nejsou nikdy fyzicky uloženy, ale při “tombstoningu” většinou bez milosti zlikvidovány, a metoda Application_Activated je volána v nové instanci naší aplikace. WP7 si jen v zásobníku pamatují, jaké aplikace a v jakém pořadí byly  spuštěny a která stránka v konkrétní aplikaci byla aktivní.

Dále je potřeba si osvojit tato pravidla:

  • Metoda Application_Activated NENÍ volána po spuštění nové (“netombstonované”) instance aplikace. Po spuštění nové (“netombstonované”) instance aplikace je volána pouze metoda Application_Launching.
  • Metoda Application_Deactivated NENÍ volána při  úplném ukončení aplikace. Při  úplném ukončení aplikace je volána pouze metoda Application_Closing.

Zjednodušeně bychom mohli odpovědnosti metod v souboru App.xaml.cs popsat tímto fragmentem kódu:

Měli bychom ale vědět, že k tombstoningu může dojít kdykoli a podle mých testů na emulátoru i reálném zařízení může být aplikace “tombstonována”, i když se zrovna obnovuje z předchozího “tombstonovaného” stavu a je v metodě “Application_Activated“.  Autoři WP7 se s “tombstoningem” moc nepatlají a bez skrupulí po určité době zlikvidují vlákna aplikace, což poznáte podle výjimky ThreadAbortException.

Také se mi nelíbí, že bych měl v tomto jediném souboru ukládat a obnovovat data pro všechny stránky (“formuláře”) aplikace. Rozhodl jsem se, že na obsluhu těchto metod rezignuju a místo životní cyklu aplikace se budu zajímat jen o životní cyklus view modelu (presentation modelu, chcete-li) u každé stránky ve WP7 aplikaci.

Dnes si ukážeme bázovou třídu pro view modely a další podpůrné třídy se službami, která nás svou spoluprací zbaví nutnosti při psaní každé stránky myslet na to, že autoři WP7 aplikací se vyžívají v recidivě nekrofilního chování u WP7 aplikací. Drahý bratr Sigmund Freud by se po prohlídce stavového automatu “tombstonované“ aplikace od architektů WP7 na své pohovce jistě tetelil radostí nad předaným šokujícím materiálem. Smile

Nejdříve si ale navrhneme minimální množinu vlastností, kterou by měl každý view model splňovat, abychom se již nemuseli životním cyklem WP7 aplikace příliš zabývat při návrhu každé stránky.

  1. Musíme být schopni nahrát data při vytvoření nového view modulu. Také bychom měli zajistit, že když dojde k “tombstoningu” aplikace ještě před získáním všech dat, nezůstane view model v nějakém nekonzistentním stavu s třetinou nahraných dat, ale po obnovení z “tombstoningu” dostane šanci nahrát data znovu.

  2. Budeme schopni při “tombstoningu” automaticky perzistovat dočasný stav každého view modelu. Od této chvíle začneme říkat dočasnému stavu stav tranzientní. V tomto článku se zabývám jen tranzientním stavem aplikace, o perzistování “trvanlivějších” dat do “isolated storage” se pobavíme v některém z dalších článků.
  3. Po obnovení aplikace z “tombstonovaného” stavu každý view model automaticky nahraje svůj tranzientní stav.
    Pro uložení a nahrání tranzientního stavu nadefinujeme rozhrani ITransientStateManager.

  4. Měli bychom dát šanci view modelům zareagovat na to, že stránka, ke které jsou přidruženy, se stala aktivní stránkou, i na to, že k nim přidružená stránka aktivní už není, ať už proto, že došlo k “tombstoningu” nebo uživatel přešel na jinou stránku v aplikaci.
    Pro tyto účely máme rozhraní IActivated a IDeactivated.

  5. Budeme mít sice bázovou třídu pro view modely, ale její použití si nebudeme vynucovat. View model může být v aplikaci kterákoli třída. I když tato třída nebude potomkem bázové třídy pro view modely, bude moci volitelně využít většinu služeb, které jsou popsány v přechozích bodech.

  6. Jedná se o view modely, měli bychom tedy na úrovní bázové třídy podporovat rozhraní INotifyPropertyChanged, které nám dovoluje notifikovat o změně hodnoty ve vlastnostech view modelu. Rozhraní INotifyPropertyChanged je ve WPF i v Silverlight aplikacích všudypřítomné, proto si navrhneme další bázovou třídu PropertyChangedBase, která nám kromě implementace rozhraní INotifyPropertyChanged přinese další užitečné služby.

 

Jaké další užitečné služby má třída PropertyChangedBase?

Nemusíme vyvolávat událost PropertyChanged zadáním jen zadáním názvu vlastnosti (RaisePropertyChanged(“UserName”)), což může vést k chybě za běhu aplikace, když uděláme překlep v názvu vlastnosti ((RaisePropertyChanged(“UseName”)), ale můžeme předat název vlastnosti ve formě lambda výrazu, jehož syntaxe je zkontrolována již kompilátorem. K tomu nám slouží metoda RaisePropertyChangedEvent(Expression> propertyDefinition). Potomci třídy PropertyChangedBase mohou o změně hodnoty vlastnosti informovat takto:

U potomků třídy PropertyChangedBase, kterými budou i naše view modely, si můžeme také zavoláním metody RaiseAllPropertiesChanged vynutit vyvolání události o změně hodnoty pro každou vlastnost. Další metodě s názvem RaisePropertiesChanged(params string[] properties) můžeme předat názvy vlastností, pro které má být vyvolána metoda PropertyChanged.

A teď si  již můžeme vytvořit slibovanou třídu ViewModelBase, která je potomkem třídy PropertyChangedBase a podporuje všechna rozhraní zmíněná dříve.

 

V konstruktoru třída ViewModelBase přijímá titulek zobrazované stránky, který je uložen do vlastnosti PageTitle.

Metoda Init z rozhraní Initialize by měla být volána vždy, když je view model vytvořen, nebo když je po obnovení aplikace z “tombstonovaného” stavu zřejmé, že view model nenahrál všechna data, což se zjistí voláním virtuální metody IsAllInitDataLoaded, která v této abstraktní třídě vždy  vrací true a čeká na to, až odvozené konkrétní view modely dosadí svou logiku, kdy je považován view model za nahraný. Metoda Init by měla být taky volána, když odvozený view model obnovená tranzientní data považuje za špatná / zastaralá a nemůže je používat, což nám dá najevo nastavením vlastnosti IsInvalidModel na true. K rychlému zjištění, jestli  je možné view model dále používat slouží derivovaná vlastnost CanUseModel, která spojuje logiku obsaženou ve vlastnosti IsInvalidModel a metodě IsAllInitDataLoaded.

 

Metoda Initialize.Init

V metodě Init nejdříve nastavíme vlastnost IsInvalidModel na true, protože naše metoda Init probíhá a kdyby došlo k “tombstoningu”, nemáme všechna data a je potřeba volat metodu Init znovu. Poté jen načteme titulek aplikace do vlastnosti AppTitle,  inicializujeme vlastnost SuppressValidating, která slouží k dočasnému potlačení ověřování platnosti data zadaných uživatelem ve view modelu, na hodnotu false. O vlastnosti SuppressValidating budeme více mluvit v dalším článku včetně vysvětlení významu metody ValidateData a vlastnosti HasValidData.

Dále ViewModelBase nastavením vlastnosti IsInvalidModel na false sděluje, že jeho inicializace proběhla, všechna data v modelu považuje za platná a dá šanci odvozeným třídám, aby inicializovaly svá data voláním chráněné virtuální metody DoInternalInit, která má ve ViewModelBase prázdnou implementaci a do které odvozené třídy dají svou specifickou logiku pro načtení dat. Vlastnost IsInvalidModel mohou samozřejmě odvozené třídy nastavit v metodě DoInternalInit opět na true, ale my ve vlastnosti IsInvalidModel hodnotu true nenecháváme, protože si přepsání metody DoInternalInit v odvozených třídách nevynucujeme a v bázové třídě ViewModelBase nemůžeme vědět, kdy odvozené třídy považují svá data za neplatná.

V metodě Init nakonec také voláním metody další chráněné virtuální metody s názvem DoInternalAsyncInit v samostatném vlákně šanci odvozeným třídám asynchronně inicializovat svá data. Metodu DoInternalAsyncInit by měly odvozené třídy používat k časově náročné inicializaci, kterou není vhodné vhodné dávat do synchronně  volané metody DoInternalInit a blokovat tak hlavní thread aplikace.

Mohlo by vás také zaujmout, že v sekci catch má speciální zacházení výjimka ThreadAbortException – tato výjimka není propagována výše, protože ji vyvolá samotné běhové prostředí Silverlightu při násilném “tombstoningu”, jak jsem poznamenal na začátku tohoto článku.

 

Rozhraní IActivated s metodou Activate a rozhraní IDeactivated s metodou Deactivate ve ViewModelBase mají jen prázdnou implementaci a čekají na to, jakou logiku do nich vloží odvozené třídy. Mimochodem  – na tomto místě bychom měli poprvé vytušit, že budeme potřebovat “hostitele” našich view modelů, který bude vědět, kdy volat metody Activate, Deactivate, Init a další.

Třída ViewModelBase explicitně implementuje rozhraní ITransientStateManager, které jsme si zavedli pro podporu automatického ukládání a nahrávání tranzientního stavu. Metody LoadState a SaveState ale po svém vyvolání jen předají řízení chráněným virtuálním metodám DoInternalLoadTransientState a DoInternalSaveTransientState, aby dali šanci i odvozeným třídám změnit způsob uložení a nahrání tranzientního stavu, i když odvozené třídy jsou většinou spokojeni s tím, že to za ně zvládne předek ViewModelBase.

 

Třída ViewModelBase ve svém statickém konstruktoru dosazuje výchozí objekt podporující rozhraní ITransientStateHelper, které je klíčové pro uložení a obnovení tranzientního stavu view modelů a na které delegují i metody DoInternalLoadTransientState  a DoInternalSaveTransientState v předchozím výpisu.

Všimněte si, že metoda DoInternalLoadTransientState po obnovení tranzientního stavu zkontroluje, jestli se dá view model používat, a pokud ne, zavolá i v této fázi metodu Init.

if (!CanUseModel)
{

  Init();
}

A nyní se podíváme na mocné rozhraní ITransientStateHelper

Je asi zřejmé, že metoda GetTransientState vrátí v objektu Dictionary tranzientní stav objektu, který jí byl předán v argumentu obj. Metoda RestoreTransientState naopak obnoví tranzientní stav objektu v argumentu obj hodnotami v argumentu savedState.

Metoda IsTransientStateEnabledForObject zjistí, jestli je možné z předaného objektu získat tranzientní stav. Jak uvidíme, odvozené view modely mohou odmítnout uložení tranzientního stavu a kdekoli v aplikaci  můžeme jejich rozhodnutí jednoduše zjistit předáním instance view modelu této metodě.

Zde je jedna z možných realizací rozhraní IStateTransientStateHelper, kterou používá i naše třída ViewModelBase.

 

Odpovědnosti metod GetTransientState a RestoreTransientState jsem již popsal, nyní jen zmíním pár specialitek v kódu třídy TransientStateHelper.

Metoda GetTransientState zjistí, jestli předaný objekt  má tranzientní stav tak, že zavolá metodu IsTransientStateEnabledForObject(obj).

Metoda IsTransientStateEnabledForObject kontroluje, jestli třída, ze které objekt pochází, nezakázala vydání tranzientního stavu tím, že je na ní aplikován atribut [NonTransientState].

Atribut NonTransientState

Atribut NonTransientState může být aplikován nejen na celou třídu, ale i na jednotlivé vlastnosti objektu, které nemají být součástí tranzientního stavu.  Metoda GetTransientState neukládá celé view modely, ale s využitím reflexe jen hodnoty jejich vlastností. Atribut NonTransientState dovoluje vyřadit vlastnosti, které v tranzientním stavu view modelu nemají co dělat. To ale není vše – přímo metoda GetTransientState dle jmenné konvence vyřadí všechny vlastnosti, jejichž suffix je v poli IGNORE_METHOD_SUFFIX_LIST

public static readonly IEnumerable IGNORE_METHOD_SUFFIX_LIST = new[] { "Command", "Action", "Helper", "Service", "SynchContext"};

Do tranzientního stavu se tak nedostanou ve view modelech se často nacházející, ale s tranzientním stavem view modelu nic nemající a navíc většinou neserializovatelné objekty jako jsou objekty ICommand (vlastnost SelectCommand, SaveCommand), delegáti  TextboxTextChangedAction, služby (ILoggerService) a další.

Metoda RestoreTransientState obnoví tranzientní stav objektu. Za zmínku stojí, že když je předaný objekt potomkem PropertyNotificationBase, tak nastavením vlastnosti notificationBase.SuppressPropertyChangedNotification na true potlačíme dočasně vyvolání události OnPropertyChanged, protože je vhodné, abychom událost nevyvolávali, když view model neobsahuje všechna data a nevíme, kdo všechno na událost reaguje a jaká další, nyní ve view modelu se nenacházející data, by chtěl načíst. Jak jsem ale psal v požadavcích na začátku článku, view model nemusí podporovat žádná rozhraní, nemusí být potomkem ViewModelBase ani PropertyNotificationBase, a proto si ani dědění z PropertyNotificationBase v této metodě nevynucujeme. Poté, co metoda RestoreTransientState obnoví hodnoty všech vlastností, má TransientStateHelper povinnost notifikovat okolí o změně hodnot všech vlastností view modelu. Jestliže je objekt s obnoveným tranzientním stavem potomkem PropertyNotificationBase, zavoláme metodu RaisePropertiesChanged(propertyNames.ToArray()), jinak se metoda RestoreTransientState pokusí dohledat opět za pomoci reflexe na objektu metodu nazvanou RaisePropertyChangedEvent, která přijímá název vlastnosti a kterou použije pro hromadnou distribuci událostí OnPropertyChanged.

Ovládací prvky obsažené (nejen) v control toolkitu jsou  přecitlivělé na pořadí vyvolávání události, a proto jsou vlastnosti seřazeny nyní tak, aby vlastnosti začínající slovem Selected vyvolávaly událost OnPropertyChanged jako poslední. Máte-li ve view modelu kolekci nazvanou Orders (všechny objednávky) a SelectedOrder (vybraná objednávka z této kolekce), je zaručeno, že událost OnPropertyChanged pro vlastnost SelectedOrder bude vyvolána až po události OnPropertyChanged pro vlastnost Orders.

Prefixy vlastností, které mají vyvolávat události jako poslední, jsou v proměnné LAST_SET_VALUE_METHOD_PREFIX. Tyto vlastnosti jsou označeny příznakem LastInit z enumerace PropertyType.

public static readonly IEnumerable LAST_SET_VALUE_METHOD_PREFIX = new[] { "Selected" };

To by pro dnešek k třídě ViewModelBase a jejím pomocníkům stačilo:

Příště se podíváme hlavně na “hostitele” view modelů, který by měl být schopen:

  • Volat na view modelech ve “správnou dobu” metody Init, LoadState, SaveState, Activate, Deactivate.
  • Připojit view modely k view (stránce).
  • Náš hostitel bude podporovat i více view modelů na jedné stránce, včetně “dědění” a sdílení použitých view modelů mezi různými view na stránce.

A také si příště vysvětlíme, proč při získání tranzientního stavu ve ViewModelbase místo objektu Dictionary používáme vlastní třídu KnownTypesDictionary. Jak možná tušíte, i název “KnownTypes” odkazuje k tomu, že mají-li být hodnoty uložené v tranzientním stavu serializovatelné, tak DataContractSerializer, používaný infrastrukturou WP7 k serializaci tranzientního stavu, musíme přesvědčit, že v objektu Dictionary jsou jen objekty z jemu “známých” tříd.

Předcházející články:

Tipy pro Windows Phone 7 aplikace I



Friday, December 17, 2010 7:25:14 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C# | Compact .Net Framework | Návrhové vzory | Silverlight | WP7


 Monday, August 23, 2010
C# - kontrola existence vlastnosti u typu dynamic bez vyvolání výjimky RuntimeBinderException.

Dan Steigerwald mě na Facebooku upozornil na článek “Challenge: Dynamically dynamic” na blogu Ayende Rahiena. Jak se můžete sami podívat, celá výzva se týká toho, jak zjistit, jestli u dané instance typu dynamic existuje vlastnost se zadaným jménem, aniž byste museli odchytávat  výjimku RuntimeBinderException, která vás na chybějící vlastnost sice drsně upozorní, ale zároveň vás nutí používat kód řízený výjimkami.

 

Jak vypadá kód detekující existenci vlastnosti s vy/zneužitím RuntimeBinderException?

   private static bool HasPropertyNaive(IDynamicMetaObjectProvider dynamicProvider, string name)
        {
            try
            {
                var callSite =
                                CallSite<Func<CallSite, object, object>>.Create(Binder.GetMember(CSharpBinderFlags.None, name, typeof(Program),
                                                         new[]
                                                                 {
                                                                     CSharpArgumentInfo.Create(
                                                                         CSharpArgumentInfoFlags.None, null)
                                                                 }));
                callSite.Target(callSite, dynamicProvider);
                return true;
            }
            catch (RuntimeBinderException)
            {

                return false;
            }

        }

A použití:

static void Main(string[] args)
        {
            dynamic testDynamicObject = new ExpandoObject();
            testDynamicObject.Name = "Testovaci vlastnost";
            Console.WriteLine(HasPropertyNaive(testDynamicObject, "Name"));
            Console.WriteLine(HasPropertyNaive(testDynamicObject, "Id"));            
            Console.ReadLine();
        }

 

Stejně jako v zadání na blogu Ayende metoda HasPropertyNaive pracuje s každým objektem dynamic skrytým za rozhraním IDynamicMetaObjectProvider. V metodě napodobíme chování kompilátoru C# – vytvoříme “kontext operace”, tzv. CallSite, které předáme hlavně tzv. “Binder” voláním tovární metody metody Binder.GetMember. Binder, v našem případě binder pro get akcesor vlastnosti, jejíž přítomnost testujeme a jejíž název jsme předali metodě HasPropertyNaive v argumentu name,  si lze zjednodušeně představit jako objekt, který je odpovědný za dohledání hodnoty vlastnosti u dynamického objektu za běhu aplikace.

U CallSite použijeme metodu Target, které předáme samotnou instanci callSite a objekt dynamic, u nějž chceme otestovat existenci vlastnosti. Jestliže vlastnost u objektu dynamic neexistuje, metoda Target vyvolá výjimku RuntimeBinderException a my vrátíme false, jinak ignorujeme návratovou hodnotu metody target a vracíme true, což je pro kód volající metodu HasPropertyNaive potvrzení, že vlastnost existuje.

Metoda HasPropertyNaive plní svůj účel, ale za cenu vyvolání výjimky RuntimeBinderException. A toho se týká právě “challenge”. Zkusme se výjimky zbavit.

Kdybychom měli testovat existenci vlastnosti jen u instancí “ExpandoObject”, měli bychom hned hotovo.

private static bool HasPropertyExpandOnly(IDynamicMetaObjectProvider dynamicProvider, string name)
{
return ((IDictionary)dynamicProvider).ContainsKey(name);
}

ExpandoObject totiž podporuje rozhraní IDictionary a klíčem v objektu Dictionary jsou názvy vlastností.

Zadání ale vyžaduje, abychom zkontrolovali přítomnost vlastnosti u ktreréhokoli objektu dynamic typu IDynamicMetaObjectProvider. Když předáte metodě HasPropertyExpandOnly instanci dynamic, která dědí z DynamicObject nebo přímo implementuje rozhraní IDynamicMetaObjectProvider, při pokusu o přetypování instance na rozhraní IDictionary dojde k výjimce.

Problém s detekcí přítomnosti vlastnosti by také zcela zmizel, kdybychom měli zaručeno, že každá instance typu IDynamicMetaObjectProvider a s ní asociovaný “DynamicMetaObject” z metody GetDynamicMemberNames vrátí seznam s názvy všech dynamických členů.

private static bool HasProperty(IDynamicMetaObjectProvider dynamicProvider, string name)
{
return dynamicProvider
                    .GetMetaObject(Expression.Constant(dynamicProvider))
                    .GetDynamicMemberNames()
                    .Contains(name);
} 

Bohužel ani to garantováno nemáme a metoda GetDynamicMemberNames u mnoha instancí dynamic bez skrupulí vrátí prázdné pole, i když vlastnosti existují.

Musíme si tedy poradit jinak.

Následuje kód metody HasProperty včetně podpůrných konstrukcí, která pracuje s libovolnou instanci typu IDynamicMetaObjectProvider a ke zjištění, zda je, či není vlastnost přítomna, nepotřebuje vyvolávat výjimku RuntimeBinderException.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Dynamic;
using Microsoft.CSharp.RuntimeBinder;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;


namespace DynamicCheckPropertyExistence
{
    class Program
    {                
        private static bool HasProperty(IDynamicMetaObjectProvider dynamicProvider, string name)
        {



            var defaultBinder = Binder.GetMember(CSharpBinderFlags.None, name, typeof(Program),
                             new[]
                                     {
                                         CSharpArgumentInfo.Create(
                                         CSharpArgumentInfoFlags.None, null)
                                     }) as GetMemberBinder;


            var callSite = CallSite<Func<CallSite, object, object>>.Create(new NoThrowGetBinderMember(name, false, defaultBinder));


            var result = callSite.Target(callSite, dynamicProvider);

            if (Object.ReferenceEquals(result, NoThrowExpressionVisitor.DUMMY_RESULT))
            {
                return false;
            }

            return true;

        }

      

    }

    class NoThrowGetBinderMember : GetMemberBinder
    {
        private GetMemberBinder m_innerBinder;        
        
        public NoThrowGetBinderMember(string name, bool ignoreCase, GetMemberBinder innerBinder) : base(name, ignoreCase)
        {
            m_innerBinder = innerBinder;            
        }
        
        public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
        {


            var retMetaObject = m_innerBinder.Bind(target, new DynamicMetaObject[] {});            
            
            var noThrowVisitor = new NoThrowExpressionVisitor();
            var resultExpression = noThrowVisitor.Visit(retMetaObject.Expression);

            var finalMetaObject = new DynamicMetaObject(resultExpression, retMetaObject.Restrictions);
            return finalMetaObject;

        }
        
    }

    class NoThrowExpressionVisitor : ExpressionVisitor
    {        
        public static readonly object DUMMY_RESULT = new DummyBindingResult();
        
        public NoThrowExpressionVisitor()
        {
            
        }

        protected override Expression VisitConditional(ConditionalExpression node)
        {
            
            if (node.IfFalse.NodeType != ExpressionType.Throw)
            {
                return base.VisitConditional(node);
            }
            
            Expression<Func<Object>> dummyFalseResult = () => DUMMY_RESULT;
            var invokeDummyFalseResult = Expression.Invoke(dummyFalseResult, null);                                    
            return Expression.Condition(node.Test, node.IfTrue, invokeDummyFalseResult);
        }

        private class DummyBindingResult {}       
    }
}

Proč se metoda HasProperty obejde bez vyvolání výjimky? Použil jsem trik, kdy objektu CallSite nepředávám přímo výchozí GetMemberBinder, ale vlastní NoThrowGetMemberBinder, který je potomkem bázové třídy GetMemberBinder z DLR. Můj NoThrowGetMember v kostruktoru přijímá další objekt GetMemberBinder, který interně použije pro zjištění hodnoty vlastnosti. Metoda HasProperty předává instanci NoThrowGetMember do konstruktoru tovární metodou Binder.CreateBinder vytvořený výchozí C# Binder, takže nemusíme v třídě NoThrowGetMember naštěstí duplikovat veškerou logiku pro přístup k vlastnosti, která je  již součástí výchozího C# Binderu.

NoThrowGetBinderMember se spoléhá na to, že při pokusu o přístup k dynamickým metodám a vlastnostem u objektu IDynamicMetaProvider třída GetMemberBinder dovoluje odvozeným třídám, aby aplikovaly vlastní logiku pro práci s “dynamickými” členy v tzv. “fallback” metodách. NoThrowGetBinderMember tedy dostane šanci dohledat vlastnost v přepsané metodě FallbackGetMember.

Metoda FallbackGetMember pracuje takto:

1. Použije metodu Bind předaného Binderu (m_innerBinder) , které předá jako první argument “DynamicMetaObject” v argumentu target a druhým argumentem je prázdné pole objektů “DynamicMetaObject”. Výchozí Binder udělá svou práci a vrátí nám další DynamicMetaObject, který si uložíme do proměnné retMetaObject.

var retMetaObject = m_innerBinder.Bind(target, new DynamicMetaObject[] {});
 

2. V retMetaObject je vyhodnocovací výraz (Expression tree) pro získání hodnoty vlastnosti, který pro vlastnost Name může vypadat zjednodušeně takto. Tučně je vyznačena část, která je odpovědná za vyvolání výjimky, jestliže vlastnost neexistuje.

IIF(ExpandoTryGetValue(Convert($arg0), value(System.Dynamic.ExpandoClass), 0, "Name", False, value), value, throw(new RuntimeBinderException("'System.Dynamic.ExpandoObject' does not contain a definition for 'Name'")))
IIF(ExpandoCheckVersion(Convert($arg0), value(System.Dynamic.ExpandoClass)), {var value; ... }, gotoCallSiteBinder.UpdateLabel)

My ale výjimku vyvolávat nechceme, a proto vlastním vizitorem NoThrowExpressionVisitor modifikujeme “expression tree” tak, že  místo vyvolání výjimky, když vlastnost neexistuje, vrátíme hodnotu statické proměnné  DUMMY_RESULT. Vlastnost Expression u proměnné retMetaObject je určena pouze pro čtení, proto vytvoříme nový DynamicMetaObject s upravenou “Expression” a původními restrikcemi a uložíme ho do proměnné finalMetaObject, která je také návratovou hodnotou metody FallbackGetMember.

 


            
            var noThrowVisitor = new NoThrowExpressionVisitor();
            var resultExpression = noThrowVisitor.Visit(retMetaObject.Expression);

            var finalMetaObject = new DynamicMetaObject(resultExpression, retMetaObject.Restrictions);
            return finalMetaObject;

Úplný kód třídy NoThrowExpressionVisitor naleznete ve výpisu výše.

Metoda HasProperty  vyvolá metodu Target na objektu CallSite a zkontroluje, zda její návratová hodnota je referenčně shodná s hodnotou v proměnné NoThrowExpressionVisitor.DUMMY_RESULT a pokud tomu tak je, vrátí false, protože nyní místo vyvolání výjimky byla vrácena zástupná hodnota signalizující “vlastnost u objektu dynamic neexistuje”, jinak vrátí true -  “vlastnost existuje”.

 

var result = callSite.Target(callSite, dynamicProvider);
if (Object.ReferenceEquals(result, NoThrowExpressionVisitor.DUMMY_RESULT))
            {
                return false;
            }
            
            

Použití metody HasProperty.

static void Main(string[] args)
        {
            dynamic testDynamicObject = new ExpandoObject();
            testDynamicObject.Name = "Testovaci vlastnost";            
            Console.WriteLine(HasProperty(testDynamicObject, "Name"));
            Console.WriteLine(HasProperty(testDynamicObject, "Id"));
            Console.ReadLine();
        }

/*Výsledek:
True 
False
*/

Zkoušel jsem metodu HasProperty použít i na zjišťování existence vlastnosti u potomků třídy DynamicObject pro zpracování rss a vše funguje dle očekávání.

“Challenge” pokořen. :-) Obvyklá poznámka na závěr – za nic neručím, kód nemusí fungovat v dalších verzích DLR, C# a .Net Frameworku, ale to vy určitě víte.:)



Monday, August 23, 2010 2:33:03 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  .NET Framework | C# | LINQ | Programátorské hádanky


 Monday, May 24, 2010
Omezení pro argumenty šablony (template) v C++ napodobující “where“ omezení pro generické argumenty v C#

Tento článek je hlavně reakce na stížnost,  kterou měl kolega-vývojář z firmy, kde vývojáři použivají C++ i C#. Stížnost byla zaměřena na to, že na rozdíl od C# není možné  v C++ zkontrolovat v době kompilace, zda argument předaný do šablony implementuje vyžadované rozhraní, nebo je potomkem námi vyžadované třídy.

Zdůrazním hned v úvodu, že v článku mluvím o “klasickém” C++ a ne o jeho .Net dialektu C++/CLI, který podporuje šablony i generiku.

Příkladem v C# může být následující třída Collection, která vyžaduje, aby za generický parametr T byla dosazena třída podporující rozhraní IBusinessObject. Podrobněji jsem podobnou kolekci už v éře př. lq. (čteme před Linqem) :-) popisoval třeba zde.

public class Collection<T> : where T : IBusinessObject
{
}

Šablony v C++ jsou i přes povrchní syntaktickou podobnost zcela odlišnou jazykovou konstrukcí při srovnání s generikou v C#, a protože jsou “uzavřené šablonové typy” generovány již v době kompilace, kompilátor sám zajistí, že typ předaný do šablony podporuje všechny námi vyžadované metody, vlastnosti a operátory. Přesto existují situace, kdy chceme garantovat, že do naší “šablonové” kolekce jsou vkládány jen objekty podporující stejné rozhraní, nebo vyžadujeme, aby vkládané objekty byly povinně potomky nějaké třídy. Nestačí nám tedy, když argument šablony podporuje metodu Commit, která má stejnou signaturu jako metoda Commit z třídy Transaction, ale chceme mít již v době kompilace ověřeno, že předaná instance je potomkem zřídy Transaction. Důvod? Třeba to, že metoda Commit z třídy Transaction má v sobě vysokoúrovňový scénář, na který spoléháte (vzor Template method, jehož název nemá nic společného s šablonami ani generikou :-) ).

Pro novou verzi jazyka C++0x bylo plánováno, že různá omezení parametru šablony bude možné vyjádřit pomocí tzv. “Concepts”. Bohužel Concepts ani v C++0x  nakonec nebudou a my si musíme vystačit s výrazovými  prostředky současné verze jazyka C++. S jednou výjimkou - v článku použiju i jednu elegantní konstrukci z C++0x (implementována ve Visual Studio 2010), statickou verifikaci pomocí klíčového slova static_assert, ale ukážu, jak snadno můžeme static_assert nahradit.

Mějme běžnou šablonu třídy Collection. V našem případě jde jen o dostatečný draft třídy Collection s konstruktorem, destruktorem a prázdnou implementací metod Add a Remove.

template
<typename T>
class Collection
{
public:
    
    ~Collection(void)
    {        
    }
    
    Collection(void)
    {
        
    }
    void Add(T& t)
    {

    }
    
    void Remove(T& t)
    {

    }
};

Za generický parametr T lze nyní substituovat “cokoli”, ale my chceme omezit typ T pouze na instance podporující rozhraní IBusinessObject.

class IBusinessObject
{


public:
    IBusinessObject(void);
    virtual void CommitChanges() = 0;
    virtual void RollbackChanges() = 0;
    virtual ~IBusinessObject(void);
};

V C++ samozřejmě za rozhraní považujeme třídu, jejíž všechny metody jsou abstraktní, a proto převod dvou omezení ze C# ( potomek třídy, implementor rozhraní (realizace) ) se nám v C++ redukuje na nalezení ekvivalentu omezení “parametr šablony musí být potomkem námi vyžadované třídy”.

K vyjádření takového omezení si zavedeme pomocnou šablonovou třídu IsDerivedTest:

#pragma once

template
    <typename Base, typename Derived>
class TestIsDerived
{
    private:
        struct Is_Derived_Helper
        {
            int dummy;
        };
        
        struct Not_Derived_Helper
        {
            int dummy;
            int dummy2;
        };    
    
    private:
        TestIsDerived(void);
        ~TestIsDerived(void);

        static Is_Derived_Helper Test(Base* base);                
        static Not_Derived_Helper Test(...);

    public:                
        enum TestResult
        {
            IsDerivedResult = ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper))))
        };
};

Co třída TestIsDerived umí?


Dva parametry šablony TestIsDerived mají výmluvné názvy – Base (bázová třída) a Derived (třída, u níž chceme zkontrolovat, zda je z Base odvozena)

Tuto třídu nechceme nikdy instanciovat, proto jsou její konstruktor a destruktor privátní.  U privátních struktur Is_Derived_Helper a Not_Derived_Helper je důležitá jen jedna věc – musí mít odlišnou velikost (sizeof(Is_Derived_Helper) != sizeof(Not_Derived_Helper ), a proto první obsahuje jednu "dummy" proměnnou typu int a druhá struktura dvě “dummy” proměnné typu int.

Při kontrole zda parametr šablony Derived je odvozen z parametru šablony Base postupujeme takto:

  1. Máme dvě privátní statické metody nazvané Test. Tyto funkce jsou jen deklarovány, jejich definici nepotřebujeme, protože nebudou nikdy zavolány. Důležité je jen to, že jedna metoda Test vrací Is_Derived_Helper  a druhá metoda Test vrací Not_Derived_Helper  - připomňme si, že jde o struktury mající různou velikost.
  2. Metoda Test, která vrací strukturu Is_Derived_Helper, přijímá jako argument pointer na parametr Base, a může tedy přijmout i pointer na Derived, pokud Derived je potomkem Base. Druhá metoda Test (Test(…)) bude vyvolána vždy, když se nepodaří Derived* převést na Base*.
  3. Do hodnoty IsDerivedResult v enumeraci TestResult uložíme výsledek výrazu ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper)))) - “voláme” metodu Test tak, že přes operátor static_cast přetypujeme konstantu 0 na pointer na Derived . Ač může vypadat přetypování 0 na Derived* jako “zvěrstvo” za účelem rychlého poslání programu do řiti říše binárního nebytí,  slovo voláme je v předchozí větě záměrně v uvozovkách, protože jak jsem již psal, k žádnému volání metody Test nikdy nedojde. V době kompilace ale kompilátor zjistí, jakou variantu metody Test by zavolal pro Derived* a my z velikosti “potenciální a nikdy skutečně nevrácené” návratové hodnoty zvolené metody Test jsme schopni poznat, jestli je Derived potomkem Base. Pokud je Derived potomkem Base, bude zvolena varianta Test(Base* base), která vrací Is_Derived_Helper, a podmínka ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper) bude pravdivá  - uložíme do IsDerivedResult hodnotu 1. Pokud Derived není potomkem Base, bude zvolena varianta metody Test(…), která vrací Not_Derived_Helper, a podmínka ((sizeof(Test(static_cast<Derived*>(0))) == (sizeof(Is_Derived_Helper) bude nepravdivá (uložíme do IsDerivedResult hodnotu 0). Znovu zdůrazňuji, že k vyhodnocení podmínky dojde již v době kompilace.

Scéna je připravena, pozveme hlavní aktéry:

Mějme dvě třídy. Třída Invoice je potomkem třídy IBusinessObject a třída NotBusinessObject (nomen omen :) ) překvapivě není.:)



class NotBusinessObject
{
public:
    NotBusinessObject(void);
    virtual ~NotBusinessObject(void);
};

#pragma once
#include "IBusinessObject.h"

class Invoice : public IBusinessObject
{
    public:
        
        virtual void CommitChanges();
        virtual void RollbackChanges();
        
        Invoice(void);
        ~Invoice(void);
};

#include "StdAfx.h"
#include <iostream>
#include "Invoice.h"

using namespace std;

Invoice::Invoice(void)
{
}


Invoice::~Invoice(void)
{
}

void Invoice::CommitChanges()
{
    cout << "Commit";
}

void Invoice::RollbackChanges()
{
    cout << "Rollback";
}
        

Naše třída Collection má přijímat jen instance potomků třídy IBusinessObject.

#pragma once
#include "TestIsDerived.h"


template
<typename T>
class Collection
{
public:
    
    ~Collection(void)
    {
     static_assert(TestIsDerived<IBusinessObject, T>::IsDerivedResult, "Invalid base class, IBusinessObject required");
    }
    
    Collection(void)
    {
        
    }
    void Add(T& t)
    {

    }
    
    void Remove(T& t)
    {

    }
};

Do destruktoru jsme vložili statickou kontrolu, kdy námi vytvořené pomocné třídě TestIsDerived substituujeme za parametr šablony Base třídu IBusinessObject a za parametr Derived přímo parametr šablony třídy Collection, který musí být vždy potomkem IBusinessObject. Do destruktoru jsme kód vložili proto, aby bylo garantováno, že při instanciaci šablony Collection ke statické kontrole vždy v době kompilace dojde – kód bychom mohli vložit i do konstruktoru, ale budeme-li mít konstruktorů více, museli bychom kontrolu duplikovat v každém konstruktoru. Statická kontrola v destruktoru spoléhá na to, že vytvořený konkrétní objekt Collection<T> je v aplikaci zlikvidován  - buď jde o likvidaci automatické (lokální) proměnné, nebo vy sami použijete operátor delete atd. Jestliže by destruktor objektu v aplikaci nikdy nebyl volán, kompilátor nebude destruktor v šablonové třídě při rozvíjení šablony instanciovat, stejně jako nikdy neinstanciuje další nepoužité metody v šabloně.

A použití:

Tento kód bez problémů projde. Invoice je potomkem IBusinessObject.

// TemplateConstraint_Console.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "Collection.h"
#include "Invoice.h"
#include "NotBusinessObject.h"

int _tmain(int argc, _TCHAR* argv[])
{    
    Collection<Invoice> col1;
    
}

Tento kód nahlásí chybu. NotBusinessObject není potomkem třídy IBusinessObject.

// TemplateConstraint_Console.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "Collection.h"
#include "Invoice.h"
#include "NotBusinessObject.h"

int _tmain(int argc, _TCHAR* argv[])
{    
    Collection<NotBusinessObject> col1;
        
}

Ve Visual Studiu 2010 dostanu v době kompilace tuto chybu:

Error    1    error C2338: Invalid base class, IBusinessObject required    c:\users\stein\documents\visual studio 2010\projects\templateconstraint_console\templateconstraint_console\collection.h    16    1    TemplateConstraint_Console

Tím je náš úkol splněn, ale jak jsem sliboval, ukážu nyní, i jak se můžeme obejít bez klíčového slova static_assert z C++0x.

Třída TestIsDerived zůstane beze změny, ale napíšeme další dvě šablony, ve kterých budeme reagovat na hodnotu IsDerivedResult z třídy TestIsDerived, což původně dělal přímo static_assert.

#pragma once
template
<int T>
struct Invalid_Base_Class
{
    public:
        Invalid_Base_Class(void)
        {

        }
        ~Invalid_Base_Class(void)
        {

        }
};

template<>
struct Invalid_Base_Class<0>
{
    private:
        Invalid_Base_Class(void);
        ~Invalid_Base_Class(void);
};
 

Šablona Invalid_Base_Class má jeden šablonový parametr typu int. Struktura není svým rozhaním zajímavá, podstatný je pro nás jen název struktury, abychom v době kompilace viděli chybovou hlášku “Špatná bázová třída”. První šablona Invalid_Base_Class je běžnou strukturou, která bude použita pro instanciaci všech hodnot typu int krome hodnoty 0. Pro hodnotu 0, o níž víme, že je uložena v hodnotě enumerace TestIsDerived::IsDerivedResult, jestliže DERIVED NENÍ potomkem BASE, je určena explicitní specializace Invalid_Base_Class<0>, která má ale privátní konstruktor, takže pokus o její vytvoření vždy již při kompilaci selže.

V hrubé podobě, tedy bez vlastního makra assert_is_derived apod., které skryje práci s našimi strukturami Invalid_base_Class a které by zde jen zamlžovalo průzračnost řešení, můžeme kontrolu na nutnost šablonového parametru T třídy Collection<T> dědit z třídy IBusinessObject přepsat takto:

#pragma once
#include "Invalid_Base_Class.h"


template
<typename T>
class Collection
{
public:
    
    ~Collection(void)
    {
        Invalid_Base_Class<TestIsDerived<IBusinessObject, T>::IsDerivedResult> invalid_base;
        invalid_base;
        
    }
    
    Collection(void)
    {
        
    }
    void Add(T& t)
    {

    }
    
    void Remove(T& t)
    {

    }
};

V destruktoru se pokusíme instanciovat strukturu Invalid_Base_Class, které jako šablonový argument předáme hodnotu v IsDerivedResult.

Collection<Invoice> opět bez problémů projde, ale při pokusu vytvořit Collection<NotBusinessObject> dostanu při kompilaci tuto chybovou zprávu:

Error    1    error C2248: 'Invalid_Base_Class<0>::Invalid_Base_Class' : cannot access private member declared in class 'Invalid_Base_Class<0>'    c:\users\stein\documents\visual studio 2010\projects\templateconstraint_console\templateconstraint_console\collection.h    14    1    TemplateConstraint_Console

Řešení by se dalo přisladit dalším syntaktickým cukrem, ale princip by měl být z článku zřejmý.
Triky s šablonami jsou úžasné a převod některých rysů generiky ze C# není zase tak problematický. Jen ta nonšalantní elegance, která je C# vlastní, v přihroublém, ale o to výkonnějším, C++ trochu chybí. :) Kdyby někoho triky se šablonami zaujaly, doporučuji ke studiu Alexendrescovu knihu Modern C++ Design: Generic Programming and Design Patterns Applied.



Monday, May 24, 2010 3:50:36 PM (Central Europe Standard Time, UTC+01:00)       
Comments [0]  C# | Nativní kód


 Wednesday, February 24, 2010
Podivné? chování při explicitním přetypování typu dynamic ve Visual Studiu 2010 RC

Na twitteru jsem psal, že si pohraju s implementací rozhraní ve třídě přes automatickou delegaci na privátní  proměnnou s využitím nového typu dynamic v C# 4.0. Jestliže se dobře pamatuji, většinou se po nějakém takovém řešení pídí Delphisté. Z příkladu níže bude asi jasné i pro ostatni, co mám předchozími hutnými větami na mysli .

Při hraní si s typem dynamic jsem ale narazil na zvláštní chování při explicitním přetypování a chtěl bych poprosit někoho dalšího z mých čtenářů o vyzkoušení stejného chování ve Visual Studiu 2010 (nejlépe nejen na RC, ale i na starší Betě 2, kterou jsem už smazal). Příklad níže je jen jednoduchý “jednosměrný” prototyp, na kterém vynikne problém s explicitním přetypováním.

Zde je mnou zmiňovaná podivnost (problém):

Mějme rozhraní IWorker:

   public interface IWorker
    {
        void DoWork();
    }

A třídu Worker, která toto rozhraní implementuje.

 class Worker : IWorker
    {
        #region Implementation of IWorker

        public void DoWork()
        {
            Console.WriteLine(GetType().ToString());
        }

        #endregion
    }

Dále máme  třídu Order, která rozhraní IWorker neimplementuje, ale má privátní proměnnou m_worker implementující toto rozhraní, kterou předá své bázové třídě DirtyCastBase. DirtyCastBase je třída, která zajistí, že bude-li klient přetypovávat instanci Order na rozhraní IWorker, tak toto přetypování projde a klient dostane jako implementora instanci m_worker.

 public class Order : DirtyCastBase
    {
        private IWorker m_worker;
        public Order() : base()
        {
            m_worker = new Worker();
            SetImplementors(m_worker);
        }
    }

Třída DirtyCastBase je potomkem třídy DynamicObject, která nám v .Net 4.0 dovoluje reagovat na “dynamická volání” a přidat jednoduše “dynamické chování” přepsáním metod začínajících písmeny Try (TryGetMember, TrySetMember ) apod. Já jsem přepsal metodu TryConvert, která se s využitím chráněné virtuální metody TryFindImplementor pokusí nalézt objekt, který podporuje rozhraní vyžadované uživatelem. Deskriptor rozhraní je předán ve vlastnosti Type argumentu binder.

public class DirtyCastBase : DynamicObject
    {
        private IEnumerable<Object> m_implementors;
        private Dictionary<Type, object> m_castContext;

        public DirtyCastBase()
        {
            m_castContext = new Dictionary<Type, object>();
        }
        

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            Type requestedType = binder.Type;            

            Tuple<bool, Object> FindResult =  TryFindImplementor(m_implementors, m_castContext, requestedType);
            
            if (FindResult.Item1)
            {
                result = FindResult.Item2;
                return true;
            }

            return base.TryConvert(binder, out result);
        }

        
        protected virtual Tuple<bool, Object> TryFindImplementor(IEnumerable<Object> implementors, Dictionary<Type, object> currentCastContext, Type requestedType)
        {
            if (implementors == null)
            {
                throw new ArgumentNullException("implementors");
            }
            if (currentCastContext == null)
            {
                throw new ArgumentNullException("currentCastContext");
            }
            if (requestedType == null)
            {
                throw new ArgumentNullException("requestedType");
            }

            object result = null;

            bool found = m_castContext.TryGetValue(requestedType, out result);
            
            
            if (!found)
            {
                
                result = (from implementor in implementors
                          where implementor != null
                          let type = implementor.GetType()
                          where requestedType.IsAssignableFrom(type)
                          select implementor).FirstOrDefault();

                found = result != null;
            }
            
            if (found)
            {
                m_castContext.Add(requestedType, result);
            }
            return new Tuple<bool, object>(found, result);

        }

        protected void SetImplementors(params object[] implementors)
        {
            SetImplementors(implementors.AsEnumerable());
        }

        protected void SetImplementors(IEnumerable<object> implementors)
        {
            if (m_implementors != null)
            {
                throw new InvalidOperationException();
                
            }
                        
            m_implementors = implementors ?? Enumerable.Empty<Object>();
        }
    }
 

V našem případě by tedy třída Order by měla dovolit přetypování na rozhraní IWorker, i když sama toto rozhraní neimplementuje. Zanedbejme nyní, že není zachována referenční identita při konverzi i že vydáváme jako implementora privátní objekt, protože pro demonstrovanou techniku to není příliš podstatné.

Tento kód ověří, že přetypování projde. Využíváme implicitní  (“bez závorek”) konverzi. Samozřejmě že je nutné instanci Order přiřadit do proměnné typu dynamic.

    class Program
    {
        static void Main(string[] args)
        {
            dynamic order = new Order();

            IWorker worker = order;

            worker.DoWork();
            Console.ReadLine();
        }
    }

Implicitní konverze projde, a jak jsem očekával, je vyvolána naše metoda TryConvert.

Když ale projde implicitní konverze, proč explicitní konverze selže a metoda TryConvert vyvolá výjimku?

 static void Main(string[] args)
        {
            dynamic order = new Order();

            IWorker worker =  (IWorker) order;

            worker.DoWork();
            Console.ReadLine();
        }

Ihned je vyvolána výjimka InvalidcastException {"Unable to cast object of type 'DynamicCastTest.Order' to type 'DynamicCastTest.IWorker'."}

Z call stacku výjimky (at CallSite.Target(Closure , CallSite , Object ) se dá odvodit, že výjimku vyhodil C# DLR binder, který se ale ani nepokusil zavolat metodu TryConvert. Metoda TryConvert by ve vlastnosti Explicit argumentu binder měla dostat příznak, že šlo o explicitní konverzi, ale tato metoda evidentně není volána.

Přitom z dokumentace metody TryConvert se dá usoudit, že by metoda TryConvert měla být při explicitní konverzi volána.

Ještě jsem zcela do detailu nestudoval všechna pravidla pro typ dynamic ve specifikaci C# 4.0, ale tohle na mě působí jako bug. Pokud projde implicitní konverze, proč by byla zcela zakázána explicitní? To vůbec není v souladu s pravidly kompilátoru pro konverze v C#:

“The set of explicit conversions includes all implicit conversions. This means that redundant cast expressions are allowed.” (C# specification sekce 6.2 Explicit conversions)

A C# DLR binder se i za běhu snaží podle mých dosavadních zkušeností vždy co co nejvěrněji napodobit chování C# kompilátoru.

Anebo už jsem dnes utahaný, nejde o žádnou anomálii a něco triviálního ve svém kódu přehlížím? :-)

Tedy znovu. Můžete někdo spustit projekt ve svém Visual Studiu (nejlépe i v Betě 2) a podělit se o výsledek?

Zde je projekt ke stažení.

Díky!



Wednesday, February 24, 2010 7:19:03 PM (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | C#