Resource Acquisition Is Initialization(줄여서 RAII)은 유명한 design patter 중의 하나로 C++ 언어의 창시자인 Bjarne Stroustrup에 의해 제안되었다.
RAII 패턴은 C++ 같이 개발자가 직접 resource 관리를 해주어야 하는 언어에서 leak 을 방지하기 위한 중요한 기법으로
해당 리소스의 사용 scope이 끝날 경우에 자동으로 해제를 해주며 exception이 발생하거나 하는 경우에도 획득한 자원이 해제됨을 보장하여
robust code 코드를 작성할 수 있다.
해당 리소스의 사용 scope이 끝날 경우에 자동으로 해제를 해주며 exception이 발생하거나 하는 경우에도 획득한 자원이 해제됨을 보장하여
robust code 코드를 작성할 수 있다.
Idea
Stack 에 Object생성을 지원하는 언어의 경우(C++) 해당 Object의 scope이 끝나면 Compiler가
암묵적으로(implicitly) clenaup 코드를 호출해 준다.
암묵적으로(implicitly) clenaup 코드를 호출해 준다.
그러므로 자원(memory, mutex, handle, etc...)을 Class의 Constructor에서 획득하고 Destructor 에서 소멸해 주고
해당 Class 를 Stack 에 생성해 주면 자원의 할당/해제가 보장이 된다.
해당 Class 를 Stack 에 생성해 주면 자원의 할당/해제가 보장이 된다.
destructor를 작성할 때 주의할 점은 해당 destructor에서 여러개의 리소스를 해제할 경우 exception을 throw 하지 않도록 하여야 한다.
exception 을 던질 경우 남은 리소스를 해제 못해서 leak 이 발생 할 수 있다.
그러므로 RAII 패턴을 구현시 해당 클래스에서 관리하는 자원은 한 개의 리소스만 관리하도록 설계한다.
exception 을 던질 경우 남은 리소스를 해제 못해서 leak 이 발생 할 수 있다.
그러므로 RAII 패턴을 구현시 해당 클래스에서 관리하는 자원은 한 개의 리소스만 관리하도록 설계한다.
Language support
C++ 언어 같이 class에 Destructor를 만들수 있고 object 를 stack 에 생성하는 것을 허용하는 언어에서만 사용가능하다.
C 는 object를 stack 에 생성할 수 있지만 Destructor와 exception 이 없으므로 사용할 수 없다.
Java 는 Destructor가 제공되지 않고 모든 Object는 Heap 에 생성되므로 직접적으로 사용할 수는 없지만 finally 키워드를 이용하여 비슷하게 동작하게 할 수 있다.
C 는 object를 stack 에 생성할 수 있지만 Destructor와 exception 이 없으므로 사용할 수 없다.
Java 는 Destructor가 제공되지 않고 모든 Object는 Heap 에 생성되므로 직접적으로 사용할 수는 없지만 finally 키워드를 이용하여 비슷하게 동작하게 할 수 있다.
Typical uses
Symbian C++ example
심비안OS 는 개발자의 실수로 memory leak 이 발생하여 전체 App 실행에 영향을 주는 이를 막기위해 Cleanup Stack이라는
RAII 비슷한 개념을 도입하였다.
RAII 비슷한 개념을 도입하였다.
CDemo* demo = new CDemo; DangerousOperationL(); delete demo; |
위의 예에서 DangerousOperationL() 에서 예외가 발생한다면 delete demo; 구문을 실행할수가 없으므로 leak 이 발생한다.
이를 막기위해 Symbian C++ 은 다음과 같은 방법을 사용한다.
이를 막기위해 Symbian C++ 은 다음과 같은 방법을 사용한다.
CDemo\* demo = new CDemo(); CleanupStack:: PushL(demo); DangerousOperationL(); CleanupStack:: PopAndDestroy() |
위 와 같이 CleanupStack()에 할당받은 객체를 등록하고 PopAndDestroy() 를 통해 해당 객체의 소멸을 보장한다.
C++ example
The following RAII class is a lightweight wrapper of the C standard library file system calls.
# include <cstdio> # include <stdexcept> // std::runtime_error class file { public : file ( const char * filename) : file_(std::fopen(filename, "w+" )) { if (\!file_) { throw std::runtime_error( "file open failure" ); } } ~file() { if (std::fclose(file_)) { // failed to flush latest changes. // handle it } } void write ( const char \* str) { if (EOF h1. std::fputs(str, file_)) { throw std::runtime_error( "file write failure" ); } } private : std::FILE* file_; // prevent copying and assignment; not implemented file ( const file &); file & operator= ( const file &); }; |
The class file can then be used as follows:
void example_usage() { file logfile( "logfile.txt" ); // open file (acquire resource) logfile.write( "hello logfile\!" ); // continue using logfile ... // throw exceptions or return without // worrying about closing the log; // it is closed automatically when // logfile goes out of scope \} |
Visual Basic example
'Internal representation holding handles etc. Private Sub Class_Initialize() 'Obtain resource. End Sub Private Sub Class_Terminate() 'Release resource. End Sub |
Visual C++ example
Windows에서 C++로 COM을 사용할 경우 CComPtr Class를 이용해서 RAII 를 사용할 수 있다.
CComptr은 Reference Counting 을 이용하여 자원을 관리한다.
CComptr은 Reference Counting 을 이용하여 자원을 관리한다.
Resource management without RAII
C Excample
C 언어는 RAII 를 사용할 수 없지만 goto 문을 이용하여 cleanup logic 을 효율적으로 구성할 수 있다.
C로 개발할 경우 함수내에서 메모리 할당이 필요할 경우 다음과 같이 코딩하는 사례가 많다.
int func() { char *a = NULL, *b = NULL ,*c = NULL; a = malloc ( sizeof ( char ) * 20); if (!a) { return FAIL; } b = malloc ( sizeof ( char ) * 10); if (!b) { free (a); return FAIL; } c = malloc ( sizeof ( char ) * 50); if (!c) { free (a); free (b); return FAIL; } doanything(); free (a); free (b); free (c); return TRUE; } |
위와 같은 경우 새로운 포인터 d 가 추가로 필요할 경우 c = malloc(sizeof(char) * 50); 뒤에 다음과 같은 추가 코드가 필요하다.
d = malloc ( sizeof ( char ) * 50); if (!d) { free (a); free (b); free (c); return FAIL; } |
함수가 복잡해질수록 실수의 여지가 많아지므로 저런 경우는 goto 를 이용하여 cleanup label 을 만드는 게 유용하다.
int func() { int ret = FAIL; char *a = NULL, *b = NULL ,*c = NULL; a = malloc ( sizeof ( char ) * 20); if (!a) { goto cleanup; } b = malloc ( sizeof ( char ) * 10); if (!b) { goto cleanup; } c = malloc ( sizeof ( char ) * 50); if (!c) { goto cleanup; } doanything(); ret = TRUE; cleanup: if (a) { free (a); } if (b) { free (b); } if (c) { free (c); } return ret; } |
Java example
Java의 경우 다음과 같이 finally 를 이용해서 작성할 수 있다.
FileInputStream fis1 = null ; FileInputStream fis2 = null ; try { fis1 = new FileInputStream( "c:/aaa.txt" ); fis2 = new FileInputStream( "c:/bbb.txt" ); // doSomeThing(); } finally { if (fis1 != null ) { fis1.close(); } if (fis2 != null ) { fis2.close(); } } |
주의할 점은 11 라인에서 Stream close 시 exception이 발생할 수 있으며 이경우 fis2 는 close에 도달하지 못한다.
그러므로 다음과 같이 try{} catch{}로 둘러 싸서 코딩하거나
그러므로 다음과 같이 try{} catch{}로 둘러 싸서 코딩하거나
FileInputStream fis1 = null ; FileInputStream fis2 = null ; try { fis1 = new FileInputStream( "c:/aaa.txt" ); fis2 = new FileInputStream( "c:/bbb.txt" ); // doSomeThing(); } finally { if (fis1 != null ) { try { fis1.close();} catch (Exceptione) {} } if (fis2 != null ) { try { fis2.close();} catch (Exceptione) {} } } |
아니면 Unconditionally close를 지원하는 library(예: Apache Commons IO,Apache DBCP 등을 사용하는게 좋다.
FileInputStream fis1 = null ; FileInputStream fis2 = null ; try { fis1 = new FileInputStream( "c:/aaa.txt" ); fis2 = new FileInputStream( "c:/bbb.txt" ); // doSomeThing(); } finally { IOUtils.closeQuietly(fis1); IOUtils.closeQuietly(fis2); } |
C# example
In C# this is accomplished by wrapping any object that implements the IDisposable interface in a using statement. When execution leaves the scope of the using statement body the Dispose method on the wrapped object is executed giving it a deterministic way to clean up any resources. <ref>http://en.wikipedia.org/wiki/RAII#Resource_management_without_RAII</ref>
public class CSharpExample { public static void Main() { using (FileStream fs = new FileStream( "log.txt" )) using (StreamWriter log = new StreamWriter(fs)) { log.WriteLine( "hello logfile!" ); } } } |
C++ 용 RAII library
smart pointer
C++ 표준에는 STL(Standard Template Library) 에 auto_ptr 이라는 smart pointer class가 있다.
이 class를 이용하여 raw pointer에 대해 RAII 패턴을 사용할 수 있다.
이 class를 이용하여 raw pointer에 대해 RAII 패턴을 사용할 수 있다.
smart pointer 미적용 코드
// Example 1(a): Original code // void f() { T* pt( new T ); //...more code... delete pt; } |
smart pointer 적용
위와 같은 코드는 // T 객체를 할당 받은후에 사용하다가 delete 를 명시적으로 해주어야 한다.
이런 코드는 auto_ptr 을 이용하여 안전한 코드로 만들수 있다.
// void f() { auto_ptr<T> pt( new T ); //...more code... } // cool: pt's destructor is called as it goes out of scope, and the object is deleted automatically |
auto_ptr 에서의 owner ship
auto_ptr 에는 owner ship을 옮길수가 있다.
예로 복사연산자를 수행(9라인)하거나 release() 를 명시적으로 호출(20라인)하거나 다른 pointr에 대해 할당연산자(=) 를 수행하면
해당 객체에 대한 owner ship이 연산자 좌측에 있는 객체로 이동하며 우측에 있는 smart pointer가 갖고 있는 객체는 무효화된다.
예로 복사연산자를 수행(9라인)하거나 release() 를 명시적으로 호출(20라인)하거나 다른 pointr에 대해 할당연산자(=) 를 수행하면
해당 객체에 대한 owner ship이 연산자 좌측에 있는 객체로 이동하며 우측에 있는 smart pointer가 갖고 있는 객체는 무효화된다.
객체의 owner ship 이동으로 인한 혼동을 방지하려면 해당 smart pointer를 const 로 선언해 준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // Example 2: Using an auto_ptr // void g() { T* pt1 = new T; // right now, we own the allocated object // pass ownership to an auto_ptr auto_ptr<T> pt2( pt1 ); // use the auto_ptr the same way // we'd use a simple pointer *pt2 = 12; // same as "*pt1 = 12;" pt2->SomeFunc(); // same as "pt1->SomeFunc();" // use get() to see the pointer value assert ( pt1 == pt2.get() ); // use release() to take back ownership T* pt3 = pt2.release(); // delete the object ourselves, since now // no auto_ptr owns it any more delete pt3; } // pt2 doesn't own any pointer, and so won't try to delete it... OK, no double delete |
auto_ptr 이 보유한 객체 재할당
auto_ptr의 reset() 멤버 함수를 통해 객체를 재할당할수 있다. reset 시 기존의 할당된 객체는 소멸된다.
void h() { auto_ptr<T> pt( new T(1) ); pt.reset( new T(2) ); // deletes the first T that was allocated with "new T(1)" } // finally, pt goes out of scope and the second T is also deleted |
boost shared_ptr
auto_ptr 의 문제점
STL에 있는 auto_ptr 은 굉장히 유용한 타입이지만 치명적인 문제가 있었다.
- STL에 있는 Container에 대해 제대로 동작하지 않음. (The C++ Programming Language 14.4.2)
- C++ 표준 만들때 마지막에 smart pointer가 급하게 들어가 기존 container와 호환성을 확보하지 못했다.(Effective STL 항목 8)
- 그러므로 vector나 list,map 같은 자료구조를 auto_ptr 과 같이 사용할 수가 없다. (http://ootips.org/yonat/4dev/smart-pointers.html#WhySTL
- array에 대해 동작하지 않음
- array의 경우 할당하는 연산자는 new[] 이고 해제하는 연산자는 delete[] 이다.
- auto_ptr은 위 두 연산자를 지원하지 않으므로 array 에 대해 제대로 동작하지 않는다. (auto_ptr 소스 참조)
- owner ship
- 한 객체에 대해 여러 포인터를 사용할수 없으므로 매번 owner ship 이 이동되어 혼란을 줌.
- 복사연산자가 deep copy 만 동작하므로 매번 객체를 새로 할당해야해서 과부하를 줄 수 있음.
- ref count 를 지원 안 함
boost library 란
c++ 의 차세대 표준에 반영을 염두로 구현한 Template Library 의 묶음. 자세한 내용은 boost 홈페이지 참조
boost에서 auto_ptr 개선
boost의 smart_ptr은 다음과 같은 객체를 제공한다.
scoped_ptr
|
<boost/scoped_ptr.hpp>
|
Simple sole ownership of single objects. Noncopyable.
|
scoped_array
|
<boost/scoped_array.hpp>
|
Simple sole ownership of arrays. Noncopyable.
|
shared_ptr
|
<boost/shared_ptr.hpp>
|
Object ownership shared among multiple pointers.
|
shared_array
|
<boost/shared_array.hpp>
|
Array ownership shared among multiple pointers.
|
weak_ptr
|
<boost/weak_ptr.hpp>
|
Non-owning observers of an object owned by shared_ptr.
|
intrusive_ptr
|
<boost/intrusive_ptr.hpp>
|
Shared ownership of objects with an embedded reference count.
|
Loki Scope Guard
Loki는 Modern C++ Design 의 저자인 Andrei Alexandrescu이 만든 template library 묶음이다.
이 라이브러리에는 다양한 기능이 있지만 임의의 Object에 대해 RAII를 적용할 수 있는 ScopeGuard 라는 Idiom 이 있다.
ScopeGuard 는 다음과 같이 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
| using namespace Loki; void func1() { FILE * hFile = fopen ( "aaa.txt" , "r" ); // guard for a static function ScopeGuard g1 = MakeGuard(& fclose , hFile); // guard for a member function ScopeGuard g2 = MakeGuard(&Object::do_sg, obj); } |
func1의 수행이 끝나면 g1에 의해 fclose(hFile) 을 자동으로 실행한다.
member 함수에 대해 ScoreGuard 할 경우 g2 처럼 수행할 수 있다.
member 함수에 대해 ScoreGuard 할 경우 g2 처럼 수행할 수 있다.
다음은 시스템에 적용된 예이다.
DB사용시 속도를 위해 미리 연결을 해서 db connection pool 을 만들어 놓고 getOTLHandle()을 통해 pool에서
DB 연결 객체를 얻어서 사용한다.
DB사용시 속도를 위해 미리 연결을 해서 db connection pool 을 만들어 놓고 getOTLHandle()을 통해 pool에서
DB 연결 객체를 얻어서 사용한다.
DB를 통해 작업할 경우 DB 연결이 끊어지거나 할 경우 재연결할 필요가 있으므로 결과값인 stat도 cleanup 함수에 전달한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| int CDatabases::updateUserHistoryById(UserInfo& user) { int stat = STAT_OK; string msg; otl_connect& db = getOTLHandle(); ScopeGuard guard = MakeObjGuard(* this , &CDatabases::ReleaseOTLHandleDML, ByRef(db), ByRef(stat)); try { do_db_proc(); // DB 사용 } catch (otl_exception & p) { stat = p.code; msg = ( char *)p.msg; } catch (...) { stat = STAT_FAIL; } return stat; } |
22 라인이 끝나면 guard 에 의해 자동으로 this->ReleaseOTLHandleDML(db, stat) 가 호출이 된다.
ReleaseOTLHandleDML의 내부에서는 pool에 사용한 db 객체를 반납하며 stat 값을 확인해서 (3113, 3114, 3115, 24324)일 경우
재연결을 시도한다.
ReleaseOTLHandleDML의 내부에서는 pool에 사용한 db 객체를 반납하며 stat 값을 확인해서 (3113, 3114, 3115, 24324)일 경우
재연결을 시도한다.
RAII를 적용할 수 있는 기타 resource
memory 뿐만 아니라 다음과 같은 자원에도 RAII 를 적용하여 resourc eleak 을 방지할 수 있다.
- file handles, which mark-and-sweep garbage collection does not handle as gracefully
- windows that have to be closed
- icons in the notification area that have to be hidden
- synchronization primitives like mutexes which must be unlocked to allow threads entry to a critical section
- network connections
댓글 없음:
댓글 쓰기