Osobně jsem se základy práce s mutexy a podmínkovými proměnnými dočetl v
tomto starodávném článku. Ten článek sice vyšel dávno před přechodem libpthread v Linuxu na NPTL, takže některé poznámky o interně používaných signálech apod. už neplatí - ale základní "o čem to je" platí myslím dál. Hrál jsem si s tou synchronizací v C a základním C++ postupně víc a víc, až jsem v jednom proprietárním prográmku měl asi 7 různých tříd objektů (které cosi obalovaly), každý měl svá 1-4 vlákna, mezi sebou si předávaly "práci" a pochopitelně se přitom všelijak zamykaly... jako houfec baletek. Fronta nebo obecně nějaká hromádka krmení, ochráněná podmínkovou proměnnou, je mocná zbraň :-) Není vůbec na škodu, myslet trochu "mimo předdefinované škatulky", a ze shůry daných základních synchronizačních primitiv si stavět složitější konstrukce na míru svému problému. Třeba více producentů pro jednoho konzumenta... podmínkovou proměnnou lze chránit prostou frontu, nebo třeba nějaký složitý key-value index s multikriteriální porovnávací funkcí... Jednalo se tehdy o nějakou komunikační gateway mezi více rozhraními, a dalo se to celé rozdrobit na datové objekty držené v indexech, které na sebe navzájem odkazovaly, kolem každého objektu tančilo několik vláken... jedním ze základních návrhových principů bylo, že každé vlákno smí delší dobu (až na neurčito) blokovat právě v jednom bodě, aby se zabránilo nepříjemnostem typu zbytečné vzájemné čekání nebo dokonce uváznutí. Tím jediným bodem mohla být podmínková proměnná (vlákno = konzument) nebo třeba čekání na vstup (událost) od fyzického I/O zařízení... Všimněte si, že při čekání na podmínkové proměnné je související mutex *odemčený*. Držet mutex dlouhodobě zamčený je hřích - přípustný pouze v případě, že to fakt nejde udělat rychleji nebo odložit na dobu, kdy zámek není potřeba (třeba když je zámkem chráněna manipulace s nějakým btree asociativním polem / indexem, byť u rozsáhlejšího btree může být třeba rebalancing procesorově a časově náročný).
BTW
Když si vezmete že jeden thread lockne resource, do toho mu kernel sebere kvantum, tak bude případný další thread viset než ho ten přerušený znova uvolní. Pro to co píšete to je jedno (desetiny sekundy za uherskej rok u příkladu na naučení se). Ale do budoucna je dobré mít pro lock/unlock makra a pak to časem udělat dobře (futexy pro linux, něco přenositelnějšího i jinam)
Možná tomu špatně rozumím, ale zrovna v tomhle případě mě držený zámek neuráží :-) Pokud má konkrétní vlákno zrovna práci na nějakém zamčeném "vzácném zdroji", třeba se delší dobu hrabe v nějakém indexu, a přeruší ho preemptivně scheduler, tak to vlákno prostě zůstane ve stavu "running". A pokud nějaká další vlákna trpělivě čekají na tentýž zámek, tak holt čekají dál a zcela po právu / smysluplně. Z pohledu scheduleru tato další vlákna "z vlastní vůle spí". Takže scheduler půjčí procesor na chvilku nějakému jinému procesu, který by také rád běžel. Halt zas chvilku někdo jiný tahá pilku. Nebo pokud se nikdo jiný o procesor nehlásí, dostane ho obratem zpátky naše původní vlákno, které se chce ještě chvilku hrabat v tom svém indexu - vlákno dostane procesor zpátky, protože je z pohledu scheduleru "running". Prostě: které vlákno má práci, a indikuje toto scheduleru, má šanci procesor dostat zpátky - a vlákna spící na podmínkových proměnných patrně spí dál zcela oprávněně, a při správném rozvržení dat a vláken je to jako celek dost slušně efektivní.
Mám na tuhle dobu a programátorské problémy hezké vzpomínky. Jak shluknout v kódu objekt (struct) a k němu náležející vlákna. Jak nastartovat vlákno a "memberizovat" ho, aby běželo jako metoda konkrétního objektu. Jak do toho objektu (třídy/structu) zakomponovat podmínkovou proměnnou a mutex, které si "vlastní" vlákna objektu budou zdvořile předávat. Jak to zamykání zabalit do "manipulačních" metod, aby další kód volající tyto metody nemusel řešit pthread primitiva. Jak z různých tříd takových "rozvlákněných objektů" skládat hierarchie / mesh topologie, a zároveň zajistit "graceful cleanup" při ukončení jednotlivého vlákna, případně kaskádovitý úklid vláken a objektů při ukončení programu... Nabízí se, zapojit do hry reference counting = osobně jsem přidával do "užitečných" objektů šablonový "invazivní" reference counting objekt, který tuším navíc obsahoval zámeček... aby ten reference counting fungoval z více vláken, a aby fungovalo "poslední zhasne" = po dekrementaci ref.counteru na nulu se objekt dále prostřednictvím zmíněného invazivního reference counteru sám destruuje... jde pouze o to zařídit, aby po dealokaci paměti
dobíhající member metoda už nesáhla do dat nyní již neexistujícího objektu :-) = dealokace dělat až těsně před návratem.
Podobně zábavné situace "slepice nebo vejce" nastávají při graceful shutdownu a řízenému zastavení všech vláken hlavní smyčkou programu, kterou přerušil signal handler apod. Resp. zařídit, aby došlo ke graceful shutdownu jak při zastavení vnějším povelem, tak při zastavení spontánním (vlákno zjistí chybu a proto se samo ukončí, případně to napřed nějak nahlásí).
Řekl bych, že tahle práce "bolí, ale hezky" :-) Ladit ty vejce a slepice je někdy záhul na mozkovnu. Ale je super, když to po vyladění celé chodí jak hodiny. A naopak není vůbec super, když to napíšete a odladíte na nějakém stroji/procesoru, pak to spustíte na jiném s troji s jiným počtem jader apod., a ono to začne padat, protože se změní "charakter konkurentního běhu více vláken" v těsném okolí zamykaných kritických sekcí :-) Je to *hodně* náročné na předvídavost a pečlivost při psaní kódu. A je to potenciálně veliká zábava.