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í.

Příklad v C++11

editovat

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]

Výhody

editovat

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]

Typické použití

editovat

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í

editovat

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.

Omezení

editovat

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]

Počítání odkazů

editovat

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]

Reference

editovat

V tomto článku byl použit překlad textu z článku Resource acquisition is initialization na anglické Wikipedii.

  1. a b STROUSTRUP, Bjarne. Why doesn't C++ provide a "finally" construct? [online]. 2017-09-30 [cit. 2019-03-09]. Dostupné online. 
  2. SUTTER, Herb; ALEXANDRESCU, Andrei, 2005. C++ Coding Standards. [s.l.]: Addison-Wesley. (C++ In-Depth Series). Dostupné online. ISBN 978-0-321-11358-0. S. 24. 
  3. Gem #70: The Scope Locks Idiom [online]. AdaCore [cit. 2021-05-21]. Dostupné online. (anglicky) 
  4. The Valadate Project. Destruction [online]. The Vala Tutorial version 0.30 [cit. 2021-05-21]. Dostupné online. 
  5. RAII - Rust By Example [online]. doc.rust-lang.org [cit. 2020-11-22]. Dostupné online. 
  6. Stroustrup 1994, 16.5 Resource Management, pp. 388–89.
  7. Stroustrup 1994, 16.1 Exception Handling: Introduction, pp. 383–84.
  8. Stroustrup 1994, s. 389. I called this technique "resource acquisition is initialization."
  9. Arthur Tchaikovsky. Change official RAII to CADRe [online]. Google Groups, 2012-11-06 [cit. 2019-03-09]. Dostupné online. 
  10. CHOU, Allen. Scope-Based Resource Management (RAII) [online]. 2014-10-01 [cit. 2019-03-09]. Dostupné online. 
  11. Richard Smith. Working Draft, Standard for Programming Language C++ [online]. 2017-03-21 [cit. 2023-09-07]. S. 151, section §9.6. Dostupné online. 
  12. How can I handle a destructor that fails? [online]. Standard C++ Foundation [cit. 2019-03-09]. Dostupné online. 
  13. Richard Smith. Working Draft, Standard for Programming Language C++ [online]. 2017-03-21 [cit. 2019-03-09]. Dostupné online. 
  14. STROUSTRUP, Bjarne; SUTTER, Herb. C++ Core Guidelines [online]. 2020-08-03 [cit. 2020-08-15]. Dostupné online. 
  15. I have too many try blocks; what can I do about it? [online]. Standard C++ Foundation [cit. 2019-03-09]. Dostupné online. 
  16. Specifying Attributes of Variables [online]. Projekt GNU [cit. 2019-03-09]. Dostupné online. 
  17. WEIMER, Westley; NECULA, George C., 2008. Exceptional Situations and Program Reliability. ACM Transactions on Programming Languages and Systems. Roč. 30, čís. 2. Dostupné online. 
  18. ildjarn. RAII and Stack unwinding [online]. Stack Overflow, 2011-04-05 [cit. 2019-03-09]. Dostupné online. 
  19. RAII na YouTube
  20. Extending Python with C or C++: Reference Counts [online]. Python Software Foundation [cit. 2019-03-09]. Dostupné online. 
  21. hobbs. Does PHP support the RAII pattern? How? [online]. 2011-02-08 [cit. 2019-03-09]. Dostupné online. 
  22. gc — Garbage Collector interface [online]. Python Software Foundation [cit. 2019-03-09]. Dostupné online. 

Literatura

editovat

Externí odkazy

editovat
  • DEWHURST, Stephen C. Gotcha #67: Failure to Employ Resource Acquisition Is Initialization [online]. [cit. 2024-09-15]. Dostupné online. 
  • VENNERS, Bill. A Conversation with Bjarne Stroustrup [online]. [cit. 2024-09-15]. Dostupné online. 
  • KARLSSON, Bjorn; WILSON, Matthew. The Law of The Big Two [online]. [cit. 2024-09-15]. Dostupné online. 
  • KALEV, Danny. Implementing the 'Resource Acquisition is Initialization' Idiom [online]. [cit. 2024-09-15]. Dostupné v archivu pořízeném z originálu dne 2009-08-15. 
  • PIBINGER, Roland. RAII, Dynamic Objects, and Factories in C++ [online]. [cit. 2024-09-15]. Dostupné online. 
  • RAII v Delphi: KELLY, Barry. One-liner RAII in Delphi [online]. [cit. 2024-09-15]. Dostupné online. 
  • W3computing. RAII in C++ [online]. [cit. 2024-09-15]. Dostupné online.