Hlavní navigace

Typické referenční cykly

15. 6. 2014 18:52 (aktualizováno) | zboj

Před uvedením typických případů, kdy mohou vzniknout referenční cykly, neuškodí stručný přehled historie a evoluce správy paměti v procedurálních jazycích. K tomu dobře poslouží Objective-C, jež prošlo téměř všemi fázemi této evoluce.

Původní ObjC i to “NeXTovské” spravovalo paměť stejně jako C. Když se třídě poslala zpráva new, dynamicky se vytvořila instance objektu. Poslání zprávy free instanci paměť uvolnilo. Potřeba sofistikovanější správy paměti vyvstala se zavedením GUI a s ním spojené smyčky událostí. Sun proto přidal do OpenStepu (hlavní OO platformy Sunu založené na NeXTStepu před uvedením Javy) tzv. autorelease pool a s ním i počítání referencí. Na začátku smyčky událostí se vytvořila nová instance poolu a na konci se zrušila, a s ní i všechny krátkodobé objekty. Objekty potřebné déle přežily díky referenci někde mimo, například v nějaké kolekci. Takže místo

[[NSString alloc] initWithFormat: …]

se psalo

[NSString stringWithFormat: …]

což je zkratka za

[[[NSString alloc] initWithFormat: …] autorelease]

díky čemuž se programátor nemusel starat o uvolňování paměti. Komu to připadá povědomé, toho nepřekvapí, že tento koncept do značné míry přejímají a rozvíjejí generační GC (také rozlišují krátkodobé a déle žijící objekty). Následně ObjC dostalo plně automatický (ale konzervativní) GC a prozatím nejvyšším stupněm evoluce je ARC, tedy automatické počítání referencí (to má například i Swift nebo microsoftí C++/CX a některé skriptovací jazyky).

Každý by měl být schopen spravovat paměť ručně a vědět, jak (de)alokace paměti funguje “pod pokličkou” (i když si nedělám iluze o mladší generaci “javistů” a “dotnetistů”), ale to neznamená, že se to tak má v kódu vyšší úrovně dělat. ARC dost zpřehlední kód (stručnější kód je z principu čitelnější) a také správu paměti značně optimalizuje (překladače pro výše uvedené jazyky negenerují kód pro aktualizaci čítače referencí, není-li to nutné, tedy ve většině případů). Navíc je dealokace ve většině případů deterministická (narozdíl od GC), takže paměťově značně efektivnější, i když cenou za to je nedeterministická alokace (generační, ale ne konzervativní GC má deterministickou alokaci). ARC ani GC tedy nejsou ve své základní formě vhodné pro real-time systémy, rychlostně na tom jsou (asymptoticky) v podstatě stejně, ale ARC má menší nároky na paměť.

A teď již k případům, kdy typicky mohou vznikat cykly referencí. Nejsnadnějším na pochopení jsou algebraické datové struktury. U stromu například můžeme chtít u vrcholů odkaz na otce, ten má ale odkazy na své syny. Ejhle, cyklus. V tomto případě je samozřejmě řešení triviální (odkaz na otce nebude počítaná reference), ale u obecného grafu už je situace malinko složitější. Tam budeme chtít nějakou kolekci, jež udrží vrcholy grafu “naživu”, a odkazy mezi nimi (tj. hrany) nebudou počítané.

I dobré programátory mohou mást bloky (uzávěry definované lambda výrazy). Obvykle se takový blok použije jako callback, a pokud jde o callback objektu, vzniká mezi ním a příslušným objektem referenční cyklus. Protože onen objekt je primární, odkaz bloku na objekt se nepočítá, je tzv. slabý. V ObjC se použije něco jako

__weak __typeof(self) me = self;

a v bloku se místo self všude použije me. Jednoduché a účinné. Pokud blok není callback (nebo jde o jednorázový callback), není referencován a cyklus tedy nehrozí.

Na samostatný článek by byl popis ARC ve vícevláknovém postředí. Do toho se pouštět nebudu, jen poznamenám (abych předešel stále se opakujícím stupidním komentářům o „pomalosti“ počítání referencí), že aktualizace čítače referencí se nesynchronizuje, pokud překladač usoudí (a téměř vždy to pozná), že nemůže dojít ke kolizi.