Paralelení simulace se ve zdrojácích zapíná #define GTGEN_WITH_MULTITHREADING.
Funkce, které jsou klony jednovláknových verzí mají příponu _st
(SimulatorThreads, _t už má knihovna pthread) a jinak se jmenují stejně.
Takové funkce vznikají spíš vyjímečně a spíše ve Scheduleru, speciálně v
buňce by neměly. Vznikají typicky tehdy, když převod jednovláknové verze na
vícevláknovou vyžaduje změnit koncepci funkce a vůbec ji prakticky celou
přepsat -- není to situce "tu a tam přidám třeba zámek a hotovo", což by
mělo být naopak typická úprava v implementaci buňky. 
Proměnné související s vícevláknovým zpracováním mají také příponu _st.


Když je zapnutý GTGEN_WITH_MULTITHREADING, main() zavolá Run_st(), která
zkontroluje kolik vláken může mít k dispozici, vytvoří je a iniciálně v každém
pustí Scheduler::RunFeeder_st(). Vznikne tak několik paralelně běžících instancí stejného
plánovače, které mají za úkol vytvářet nové úlohy jakmile nějaká skončí (a
udržovat tak všechny cores pořád v činnosti/vytížené). Během simulace bude
ale v simulátoru klasicky pouze jedna instance samotného Scheduleru, bude
jenom vícekrát běžet Scheduler::RunFeeder_st(). Scheduler::Run_st() musí
počkat až skončí všechna vytvořená vlákna (join ve for cyklu). Tahle funkce
nepoběží paralelně.

Scheduler::RunFeeder_st() je re-entratní, thread-safe. Implementuje
plánovací politiku klasického Scheduler::Run() ale pouze nad buňkami, které
nejsou aktuálně zpracovávané (takže může znovu zavolat i právě
skončenou/pozastavenou buňku). Do buněk se teda musí zavést příznak
Cell::isRunning_st (1=ano, dělám nějakou svoji fázi; 0=ne, čekám až budu
spuštěna). Čtení a modifikace tohoto příznaku bude přes společný zámek ve
Scheduleru, zámek Scheduler::schedulingLock_st. Ten nakonec bude i
zajišťovat thread-safety funkce RunFeeder_st(). Tahle funkce se dá označit
jako "task-feeder". RunFeeder_st() má ale nový význam svého parametru, není
to kolik frames se má udělat, ale u kterého frame se má zastavit. Funkce
potom končí, když se Scheduler::timeStamp dostane za zadanou hodnotu. Tohle
číslo, kdy se má zastavit, spočítá práce Scheduler::Run_st().


Scheduler::schedulingLock_st zajišťuje výlučný přístup k proměnným
souvisejícím s plánováním a spouštěním atomických funkcí buněk. Typicky
pomáhá synchronizovat ve funkci Cell:DoNextPhase(), která mění příznak
Cell::isRunning_st, a potom v Scheduler::RunFeeder_st(), aby v jednu chvíli
běžela jenom jedna (=bežela někde v kódu RunFeeder_st(), ostatní
RunFeeder_st() budou ve skutečnosti provádět subrutinu nějaké buňky). Tento
zámek synchronizuje "interakci" plánovačů RunFeeder_st().


Scheduler::sceneLock_st zajišťuje výlučný přístup k proměnným scény, což by
měl být jediný společný prostor, o který se buňky zajímají. Buňka během své
atomické funkce řeší své lokální problémy. Mimo jiné ale buňka mění svůj
tvar a sune se po scéně, což vyžaduje "interakci" s ostatními buňkami, což
znamená konzultovat (číst a zapisovat) masku scény. Scéna může být ale také
měněna plánovačem, když například "vysypává" staré scény a alokuje nové
(tedy mění také proměnné typu Scheduler::oldestScene atd.). Tento zámek se
netýká pouze masek scény, ale i všech ostatních scénových obrázků, např.
fantomy. Tento zámek synchronizuje "interakci" buněk.


Žádná funkce by nikdy neměla zamykat více zámků najednou (jakoby
vnořovat zamykání)! Podmínku jsem zavedl čistě z programátorské
pohodlnosti, asi nutně nevyplývá z analýzy běhu simulátoru.


Je tu možnost zamykat jenom portion of sceneMasks, aby se nestalo, že jinak
paralelně běžící buňky se budou defacto serializovat při čekání na zámek od
sceneMasks (Scheduler::sceneLock_st). Paralelizace by tak nic nezrychlila.
|
Jak to provést technicky? Něčím jako region-bounded locks?
Asi bude zavedena struktura informující o zamknutých regionech a přístup k této
struktuře bude řízen zámkem (kvůli data-races). Region bych charakterizoval
primitivně středem koule o poloměru cca 2.5 cellDiameter, ta koule vyznačuje
region zájmu nějaké právě se pohybující buňky. Pokud se dvě buňky chtějí
hýbat, tzn. chtejí zkoumat obsah sceneMasks, tak první buňka zadá kouli
(zamkne sub-region scény) a další buňka může pracovat jen tehdy, pokud její
střed nespadá do této koule.
Zatímco práce se scénou obvykle buňkám docela trvá (a během toho by měli celou
scénu zamklou jenom pro sebe), nyní by se zamykalo na podstatně kratší časy
pouze z důvodu aktualizace seznamu zamknutých sub-regionů. Najednou by se
mohlo hýbat více buněk (trávit čas ve svých úsecích kódu práce se scénou)
budou-li dostatečně daleko od sebe.
|
Plánovač by, pokud má na výběr více adeptů ke spuštění, mohl zohlednit jejich
vzájemné vzdálenosti a vybrat spíše ty vzdálenější. V opačném případě, při
paralelním běhu více buněk-sousedů, jejich souběh pravděpodobně nic moc
nezrychlí.


Atomické funkce buňky typicky pracují se Scheduler::sceneLock_st. Ty kratší
mitotické speciální (a komplikované) funkce věsměs zamknou scénu na svém
začátku a odemnknou na svém konci. Ty dlouhé interfázní (a pracující
relativně dle stejné šablony) vesměs zamykají scénu skutečně pouze ve
chvílích práce se scénou (G1, kde funkce roste, je z tohoto pohledu teda
docela problematická).


Komplikované atomické funkce můžou uvažovat o pthread_attr_setstacksize(),
aby si zajistili dostatečně velký prostor pro svou práci.


Jak vyřešit ukončení? Co když spadne atomická funkce při zamčené scéně?
Jak to detekovat a jak scénu potom vysypat?
Seznam aktuálních threadů, jejich handlery, budou uloženy ve vektoru
Scheduler::threadsRunning_st. Scheduler::Run_st() to naplní (po každém create, a
odstraní z něj po každém join) a Scheduler::Stop_st() si vyžádá ukončení (cancel)
všech ve vektoru (i toho co vlastně spadl nebo prostě již neběží, snad si s
tím OS nějak poradí). Ten Stop_st() se zavolá z main() po části s
vyjímkami, takže se vypíše vyjímka a potom se požádá OS, aby ukončil
zbývající ostatní vlákna. Potom teoreticky již nic neběží a zámek
Scheduler::sceneLock_st přestane mít smysl, funkce
Scheduler::ReleaseOldScene() když volána s parametrem true bude proto tento
zámek ignorovat (s true je to stejně voláno jenom v krizových vyklízecích
situacích po vyjímce, což je právě tento případ).


Jak logovat?
Vše do jednoho aktuálního logu, každá řádka ale musí obsahovat
identifikátor vlákna (a nejlépe i časový otisk), aby se tento soubor
dal posléze rozdělit na více souborů najednou... Pro kratší log se časový
otisk vypíše jenom na začátku a konci Cell::DoNextPhase() jako extra
samostatná logovací informace. Tím se určí interval, kdy co běželo a dá se
zjistit v libovolnou chvíli co právě běželo a ve kterém vlákně. Existuje
pthread_self() k zjištění ID aktuálního vlákna.

