힙 기반 자원에 대해서 auto_ptr과 tr1::shared_ptr 클래스로 관리하는 내용을 알아봤었다.
그러나 모든 자원은 힙에서 생기지 않고 힙에 생기지 않는 자원을 스마트 포인터로 처리해주는 것 또한 말이 되지 않는다.
항상 그런 것은 아니지만 자원 관리 클래스를 스스로 만들어야 할 필요를 느끼는 경우들이 있다.

 

예를 들어 Mutex 타입의 뮤텍스 객체를 조작하는 C API를 사용하는 중이라고 가정해보자.
C API에서 제공하는 함수 중엔 lock 및 unlock이 있다.

 

1
2
3
void lock(Mutex* pm);  //pm이 가리키는 뮤텍스에 잠금을 건다.
 
void unlock(Mutex* pm); //pm이 가리키는 뮤테스의 잠금을 푼다.
cs

 

그런데 뮤텍스 잠금을 관리하는 클래스를 하나 만들고 싶은데 이는 뮤텍스 잠금을 잊지 않고 풀어 주고싶기 때문이다.
이런 용도의 클래스는 기본적으로 RAII 버칙을 따라 구성한다. 즉, 생성 시에 자원을 획득하고 소멸 시에 자원을 해제하는 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Lock
{
public:
    explicit Lock(Mutex* pm) : m_pMutex(pm)
    {
        lock(m_pMutex); //자원 획득
    }
 
    ~Lock()
    {
        unlock(m_pMutex); //자원 해제
    }
 
private:
    Mutex* m_pMutex;
};
 
cs

 

사용자는 이제 Lock을 사용할 때 RAII 방식에 맞춰 사용하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
void Func()
{
    Mutex m;        //뮤텍스 정의
 
    ...
 
    {                //임계 영역을 정하기 위해 블록 생성
        Lock(&m);    //뮤텍스 잠금
 
        ...
    }                //Block이 끝나면서 뮤텍스에 걸렸던 잠금이 자동으로 풀림.
                    
}
cs

 

여기까지 보면 문제가 없을 것 같지만 만약 Lock 객체가 복사가 된다면 어떤 동작이 이루어져야 할지 생각해봐야 한다.

 

1
2
Lock m1(&m); //m에 잠금을 건다.
Lock m2(m1); //m1을 m2로 복사한다.
cs

 

선택 1 : 복사를 금지한다.

 

 RAII 객체가 복사되도록 놔두는 것 자체가 말이 안되는 경우가 꽤 많다.
 앞에 항목6에서 했던 것처럼 복사 연산을 private 멤버로 만들어준다.

 

선택 2 : 관리하고 있는 자원에 대해 참조 카운팅을 수행한다.

 

 자원을 사용하고 있는 마지막 객체가 소멸될 때까지 자원을 해제시키지 않아야하는 경우도 종종 있다.
 그렇다면 자신의 자원 관리 클래스에 참조 카운팅 방식의 복사 동작을 넣고 싶을 때, tr1::shared_ptr을 데이터 멤버로 넣으면

 간단히 해결될 것이다. 즉, Lock이 참조 카운팅 방식으로 돌아가면 좋을 것 같다고 생각하여 m_pMutex의 타입을 Mutex*에서

 tr1::shared_ptr<Mutex>로 바꾸라는 것이다. 그러나 아쉽게도 tr1::shared_ptr은 참조 카운트가 0이 될 때 자신이 가리키는 객체를

 삭제해 버리도록 되어있다. 내가 원한 것은 Mutex를 다 썼을 때 잠금만 해제하는 것이지 삭제까지는 하고 싶은 것은 아니였다.

 

 다행히도 tr1::shared_ptr이 '삭제자' 지정을 허용한다는 점이다. 삭제자란, tr1::shared_ptr이 유지하는 참조 카운트가 0이 되었을 때

 호출되는 함수 혹은 함수 객체를 말한다. (auto_ptr은 삭제자 기능 없음.)

 삭제자는 tr1::shared_ptr 생성자의 두 번째 매개변수로 선택적으로 넣어 줄 수 있다.

 그래서 하기와 같은 클래스 형태로 변경될 수 있다.

 

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
27
28
29
30
31
32
33
34
35
void lock(std::_Mutex* pm)
{
    pm->_Lock();
}
 
void unlock(std::_Mutex* pm)
{
    pm->_Unlock();
}
 
class Lock
{
public:
    explicit Lock(std::_Mutex* pm) : m_pMutex(pm, unlock) //삭제자로 unlock 함수 설정.
    {
        lock(m_pMutex.get()); //get에 대한 것은 다음 항목에서 설명.
    }
 
private:
    std::tr1::shared_ptr<std::_Mutex> m_pMutex; //shared_ptr 사용.
};
 
 
int _tmain(int argc, _TCHAR* argv[])
{
    std::_Mutex mutex;
 
    {
        Lock oLock(&mutex);
 
        //현재 Block 동안에는 Lock이 걸려져 있다.
    }
 
    return 0;
}
cs

 

 여기서 특이한 점은 Lock 클래스의 소멸자가 없어졌다는 점이다. 이유는 필요 없기 때문이다.

 항목5에서 얘기했듯이 클래스의 소멸자는 비정적 멤버 변수들의 소멸자를 자동으로 호출해준다.

 m_pMutex 스마트 포인터도 비정적 멤버 변수이기 때문에 Lock 객체가 소멸될 때 자동으로 소멸이 된다.

 

 즉, m_pMutex는 참조 카운트가 0이 되면 소멸되는 것이 아니라 삭제자로 지정된 unlock 함수를 호출하여

 Mutex 잠금을 해제하고 나서 해당 Lock 객체가 소멸될 때에 비로서 Mutex 멤버 변수는 해제되는 것이다.

 

 여기서 주의할 점

 만약 외부에서 다른 shared_ptr 포인터가 mutex를 참조하고 있다면 어떻게 되는 것일까??

 하기 주석 참고...

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int _tmain(int argc, _TCHAR* argv[])
{
    std::_Mutex mutex;
    std::tr1::shared_ptr<std::_Mutex> pMutex;
 
    {
        Lock oLock(&mutex); //Lock 동작.
 
        pMutex = oLock.GetPtr(); //oLock 객체의 스마트 포인터 반환. (mutex를 가리키고 있는 스마트 포인터)
                                 //이 구간에서 pMutex와 oLock의 m_pMutex 2개의 스마트 포인터가 mutex를 가리키고 있다.
    
    }                            //Block이 종료되면서 oLock 객체는 소멸되기 때문에 자동적으로 m_pMutex 또한 소멸된다.
                                 //그런데... 아직 pMutex는 mutex를 가리키고 있는 중이다. (즉, mutex를 pMutex만 가리키게 됨) 
                                 //Lock 클래스에서 m_pMutex의 삭제자로 지정한 unlock은 mutex를 가리키는 참조자가 0개여야지만
                                 //삭제자를 호출한다. 그러므로 이 구간이 종료되어 oLock 객체가 소멸되었지만 unlock 함수는 호출되지
                                 //않는다.
 
    ...                          //여러가지 처리 중....
 
    return 0;                     
 
}                                //이 부분에 도달해야 pMutex 또한 소멸되어진다. 그러므로 mutex를 가르키고 있는 객체는 0개가 되고
                                 //unlock 함수가 호출된다.
                                 //이렇게 되면 결국 lock 범위는 내가 생각한 것처럼 되지 않는다.
cs

 

선택 3 : 관리하고 있는 자원을 진짜로 복사한다.

 

 때에 따라서는 자원을 원하는 대로 복사할 수도 있다. 이때는 자원을 다 썼을 때 각각의 사본을 확실히 해제해줘야 한다.
 자원 관리 객체를 복사하면 이 객체가 관리하는 자원까지 모두 복사가 되어야 한다.
 즉, 깊은 복사를 수행해야 한다는 이야기이다.

 

선택 4 : 관리하고 있는 자원의 소유권을 옮긴다.

 

 흔한 경우는 아니지만, 특정원 자원에 대해 실제로 참조하는 RAII 객체가 딱 하나만 존재하도록 만들고 싶다면,

 객체가 복사될 때 그 자원의 소유권을 사본 쪽으로 아예 옮겨야 한다. auto_ptr이 바로 이런 방식이다.

 

 

2줄 요약

 

 - RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의
   복사 동작이 결정된다.

 

 - RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해 주는 선으로 마무리하는 것이다.

   하지만 이 외의 방법들도 가능하니 참고하자.

+ Recent posts