\


 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#