RAII

From Wikipedia, the free encyclopedia

Remove ads

Resource acquisition is initialization (RAII, česky osvojení prostředku je inicializace)[1] je způsob programování[2] používaný v několika objektově orientovaných, staticky typovaných programovacích jazycích. RAII je správa prostředku spjatá s životním cyklem objektu; získání prostředku (anglicky acquire) je spojené s inicializací objektu, a uvolnění prostředku s likvidací objektu. V RAII je držení prostředku invariantem třídy, a je svázané s životností objektu. Přidělování prostředků (nebo osvojování) se provádí při vytváření objektu (konkrétně při jeho inicializaci), konstruktorem, zatímco vracení (uvolnění) prostředku se provádí při ničení objektu (konkrétně při jeho finalizaci) destruktorem. Jinými slovy, aby inicializace byla úspěšná, osvojení prostředku musí uspět. Je tedy zaručené, že prostředek bude držen mezi ukončením inicializace a začátkem finalizace (držení prostředků je invariantem třídy), a že bude držen pouze v době, kdy je objekt naživu. Pokud tedy nedochází k úniku objektu, nedochází ani k úniku prostředků.

RAII je nejvíc spojován s jazykem C++, kde vznikl, ale také s programovacími jazyky Ada,[3] Vala[4] a Rust.[5] Techniku pro správu prostředků bezpečnou pro výjimky vyvinul v C++ Bjarne Stroustrup a Andrew Koenig[6] v letech 1984–89[7] a termín samotný poprvé použil Stroustrup.[8]

Pro tuto techniku se používají i jiné názvy, jako Constructor Acquires, Destructor Releases, CADRe (konstruktor získává, destruktor uvolňuje),[9] a jeden ze stylů použití se nazývá Scope-based Resource Management (SBRM (Správa prostředků založená na rozsahu platnosti),[10] což je název pro speciální případ použití automatických proměnných. RAII svazuje prostředky s životností objektu, který vždy nemusí odpovídat vstupu do rozsahu platnosti a jeho opuštění. (Zejména dynamicky alokované proměnné mají dobu života nezávislou na určitém rozsahu platnosti.) Použití RAII pro automatické proměnné (SBRM) je však nejobvyklejším případem použití.

Remove ads

Příklad v C++11

Následující příklad v C++11 ukazuje použití RAII pro přístup k souboru a zamykání mutexu:

#include <fstream>
#include <iostream>
#include <mutex>
#include <stdexcept>
#include <string>

void WriteToSouboru(const std::string& message) {
  // |mutex| chrání přístup k |souboru| (sdílenému více thready).
  static std::mutex mutex;

  // Před přístupem k |souboru| zamknout |mutex|.
  std::lock_guard<std::mutex> lock(mutex);

  // Zkus otevřít soubor.
  std::ofstream soubor("example.txt");
  if (!soubor.is_open()) {
    throw std::runtime_error("nelze otevřít soubor");
  }

  // Zápis do |souboru|:
  soubor << message << std::endl;

  // Při opuštění rozsahu platnosti bude nejdříve zavřen |soubor| (i v případě výjimky),
  // pak bude odemčen |mutex| (z jeho destruktoru) (i v případě výjimky).
}

Tento kód je bezpečný vůči výjimkám, protože C++ zaručuje, že všechny objekty s automatickým trváním (tj. proměnné lokální ve funkcích) budou zničeny při opuštění jejich rozsahu platnosti v opačném pořadí, než byly vytvořeny.[11] Je tedy zaručené, že při návratu z funkce budou zavolány destruktory objektů zámku i souboru, bez ohledu na to, zda došlo k výjimce nebo ne.[12]

Lokální proměnné umožňují snadnou správu více prostředků v rámci jedné funkce: jsou zničeny v opačném pořadí, než v jakém byly vytvořeny, a objekt je zničen pouze tehdy, když byl plně zkonstruován – tj. pokud pokud se z jeho konstruktoru nešíří žádná výjimka.[13]

Použití RAII značně zjednodušuje správu prostředků, snižuje rozsah kódu a pomáhá zajistit korektnost programu. RAII je proto doporučován standardními průmyslovými směrnicemi,[14] a většina standardní knihovny C++ se jím řídí.[15]

Remove ads

Výhody

Výhodou RAII jako techniky správy prostředků je zapouzdření, bezpečnost při výskytu výjimek (při použití objektů na zásobníku) a lokálnost (logika získání a vrácení může být napsána na jednom místě).

Zapouzdření je zajištěno tím, že logika správy prostředků je definována ve třídě pouze na jednom místě, nikoli v každém místě volání. Bezpečnost pro výjimky pro prostředky spjaté s objekty na zásobníku (tj. prostředky, které jsou uvolňovány ve stejném rozsahu platnosti tím, že prostředek je vázán na životnost proměnné na zásobníku (lokální proměnné deklarované v daném rozsahu platnosti): pokud je vyhozena výjimka, a je dedonováno správné zpracování výjimek, jediným kódem, který se provede při opouštění aktuálního rozsahu platnosti jsou destruktory objektů deklarovaných v tomto rozsahu platnosti. A konečně, lokálnost definice je zajištěna tím, že definice konstruktoru a destruktoru jsou na jednom místě v definici třídy.

Správa prostředků proto musí být svázána s dobou životnosti vhodných objektů, aby se dosáhlo automatického přidělování a uvolňování. Prostředky jsou získány během inicializace, když není šance, že by byly použity dříve, než jsou dostupné, a uvolněny při zničení stejných objektů, která zaručeně proběhne i při výskytu chyb (výjimek).

Při porovnávání RAII s konstrukcí finally používanou v Javě Stroustrup napsal, že „V reálných systémech existuje mnohem více získání prostředků než druhů prostředků, takže technika RAII vede k menšímu rozsahu kódu než použití konstrukce finally.“[1]

Remove ads

Typické použití

Přístup RAII se často používá pro řízení zámků mutexů ve vícevláknových aplikacích. Při takovém použití objekt uvolní zámek, při svém zničení. Bez RAII by v tomto scénáři bylo vysoké nebezpečí deadlocku a logika pro uzamčení mutexu by byla daleko od logiky pro jeho odemčení. S použitím RAII, kód, které zamyká mutex v zásadě obsahuje logiku, že zámek bude uvolněn, když program opustí rozsah platnosti RAII objektu.

Dalším typickým příkladem je práce se soubory: Mohli bychom mít objekt, který reprezentuje soubor otevřený pro zápis, přičemž soubor je otevřen v konstruktoru a zavřen, když program opustí rozsah platnosti objektu. V obou případech RAII zajišťuje pouze to, aby byl daný prostředek vhodně uvolněn; přesto je třeba dbát na zachování bezpečnosti při zpracování výjimek. Pokud kód modifikující datovou strukturu nebo soubor není bezpečný vůči výjimkám, mohlo by dojít k otevření mutexu nebo k uzavření souboru s poškozením datové struktury nebo souboru.

Vlastnictví dynamicky alokovaných objektů (paměť alokovaná pomocí new v C++) lze také řídit pomocí RAII tak, že objekt je uvolněn, když objekt RAII (umístěný na zásobníku) zaniká. K tomuto účelu standardní knihovna C++11 definuje třídy Smart pointerů std::unique_ptr pro objekty s jedním vlastníkem a std::shared_ptr pro objekty se sdíleným vlastnictvím. Podobné třídy jsou také dostupné prostřednictvím std::auto_ptr v C++98, a boost::shared_ptr v knihovnách Boost.

Pomocí RAII lze také posílat zprávy do sítě. V tomto případě by RAII objekt odeslal zprávu na síťový soket na konci konstruktoru, když je dokončena jeho inicializace. Také by odeslal zprávu na začátku destruktoru, než bude objekt zničen. Takový konstrukt by mohl být používán v klientském objektu na vytvoření spojení se serverem běžícím v jiném procesu.

Překladač „cleanup“ rozšíření

Clang i GCC implementují nestandardní rozšíření jazyka C pro podporu RAII: atribut cleanup proměnné.[16] Následující příklad anotuje proměnná daným destruktorem, který se zavolá, když program opouští rozsah platnosti proměnné:

void example_usage() {
  __attribute__((cleanup(fclosep))) FILE *logfile = fopen("logfile.txt", "w+");
  fputs("hello logfile!", logfile);
}

V tomto příkladě překladač zařídí, aby byla funkce fclosep zavolaná na souboru logfile před návratem z funkce example_usage.

Remove ads

Omezení

RAII funguje pouze pro prostředky získané a uvolněné (přímo nebo nepřímo) pomocí objektů na zásobníku, kde je dobře definovaná statická životnost objektu. Objekty umístěné na haldě, které samy získávají a uvolňují prostředky, jsou běžné v mnoha jazycích včetně C++. RAII vyžaduje, aby objekty na haldě byly implicitně nebo explicitně zrušeny na všech možných trajektoriích provádění, aby se provedl destruktor pro uvolnění přiřazených prostředků (nebo ekvivalent).[17]:s.8:27 Toho lze dosáhnout pomocí smart pointeru pro řízení všech objektů na haldě, přičemž pro cyklicky odkazované objekty se používají slabé ukazatele.

V C++ je zaručeno, že při výskytu výjimky dojde k postupnému uvolnění zásobníku pouze v případě, když je výjimka někde zachycena. Důvodem je, že „Pokud není v programu nalezen vyhovující handler, je zavolána funkce terminate(); zda je zásobník uvolněn před zavoláním terminate(), je implementačně závislé (15.5.1).“ (norma C++03, §15.3/9).[18] Toto chování je obvykle přijatelné, protože zbývající prostředky jako paměť, soubory, sokety, atd., uvolní při ukončení programu operační systém.[zdroj?]

Jonathan Blow na konferenci Gamelab v roce 2018 ukázal, že používání RAII může vést fragmentaci paměti, která může způsobit výpadky cache a stonásobné i větší zhoršení výkonu.[19]

Remove ads

Počítání odkazů

V Perlu, Pythonu (v implementaci CPython)[20] a v PHP[21] je životnost objektu řízena počítáním odkazů, což umožňuje použít RAII. Objekty, které už nejsou referencované, jsou okamžitě zničeny nebo finalizovány a uvolněny, takže destruktor nebo finalizátor může také prostředek uvolnit. V těchto jazycích to však není vždy idiomatické a v jazyce Python se to výslovně nedoporučuje (ve prospěch kontextových manažerů a finalizátorů z balíčku weakref).[zdroj?]

Doba života objektu nemusí být vždy vázána na nějaký rozsah platnosti, a objekty mohou být likvidovány nedeterministicky nebo vůbec. To může způsobovat náhodné úniky prostředků, které měly být uvolněny na konci nějakého rozsahu platnosti. Objekty uložené ve statických proměnných (především v globálních proměnných) nemusí být finalizovány při skončení programu, a případné s nimi spjaté prostředky tak nebudou uvolněny; například CPython finalizaci takových objektů nezaručuje. Také objekty s kruhovými odkazy nebudou při použití jednoduchého čítače referencí uvolněny, a budou žít do skončení programu; i kdyby byly uvolňovány (sofistikovanějším garbage kolektorem), doba a pořadí jejich ničení budou nedeterministické. V CPython existuje detektor cyklů, který detekuje cykly a finalizuje objekty v cyklu, i když CPython před verzí 3.4, cyklické struktury nejsou sbírány, pokud některý objekt v cyklu má finalizátor.[22]

Remove ads

Odkazy

Loading related searches...

Wikiwand - on

Seamless Wikipedia browsing. On steroids.

Remove ads