2016년 3월 20일 일요일

[c++고급] RAII (Resource Acquisition Is Initialization) 패턴

Resource Acquisition Is Initialization(줄여서 RAII)은 유명한 design patter 중의 하나로 C++ 언어의 창시자인 Bjarne Stroustrup에 의해 제안되었다.
RAII 패턴은 C++ 같이 개발자가 직접 resource 관리를 해주어야 하는 언어에서 leak 을 방지하기 위한 중요한 기법으로
해당 리소스의 사용 scope이 끝날 경우에 자동으로 해제를 해주며 exception이 발생하거나 하는 경우에도 획득한 자원이 해제됨을 보장하여
robust code 코드를 작성할 수 있다.

Idea

Stack 에 Object생성을 지원하는 언어의 경우(C++) 해당 Object의 scope이 끝나면 Compiler가
암묵적으로(implicitly) clenaup 코드를 호출해 준다.
그러므로 자원(memory, mutex, handle, etc...)을 Class의 Constructor에서 획득하고 Destructor 에서 소멸해 주고
해당 Class 를 Stack 에 생성해 주면 자원의 할당/해제가 보장이 된다.
destructor를 작성할 때 주의할 점은 해당 destructor에서 여러개의 리소스를 해제할 경우 exception을 throw 하지 않도록 하여야 한다.
exception 을 던질 경우 남은 리소스를 해제 못해서 leak 이 발생 할 수 있다.
그러므로 RAII 패턴을 구현시 해당 클래스에서 관리하는 자원은 한 개의 리소스만 관리하도록 설계한다.

Language support

C++ 언어 같이 class에 Destructor를 만들수 있고 object 를 stack 에 생성하는 것을 허용하는 언어에서만 사용가능하다.
C 는 object를 stack 에 생성할 수 있지만 Destructor와 exception 이 없으므로 사용할 수 없다.
Java 는 Destructor가 제공되지 않고 모든 Object는 Heap 에 생성되므로 직접적으로 사용할 수는 없지만 finally 키워드를 이용하여 비슷하게 동작하게 할 수 있다.

Typical uses

Symbian C++ example

심비안OS 는 개발자의 실수로 memory leak 이 발생하여 전체 App 실행에 영향을 주는 이를 막기위해 Cleanup Stack이라는
RAII 비슷한 개념을 도입하였다.
CDemo* demo = new CDemo;
DangerousOperationL();
delete demo;
위의 예에서 DangerousOperationL() 에서 예외가 발생한다면 delete demo; 구문을 실행할수가 없으므로 leak 이 발생한다.
이를 막기위해 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 을 이용하여 자원을 관리한다.

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{}로 둘러 싸서 코딩하거나
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 패턴을 사용할 수 있다.

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가 갖고 있는 객체는 무효화된다.
객체의 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 처럼 수행할 수 있다.
다음은 시스템에 적용된 예이다.
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)일 경우
재연결을 시도한다.

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

댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...