Critical Section Block 2

이전 포스트에서 CSBLOCK 매크로를 사용해서 좀 더 간편하게 크리티컬 섹션을 정의할 수 있도록 했었다. 이에 조금 덧붙여서, 어떤 데이터형이든지, 그에 대응하는 크리티컬 섹션 변수를 만들어 줄 수 있도록 하고 싶었다. 이 작업도중 Variadic macro to count number of arguments을 읽게 되었고, 옳타구나 하고 여러 개의 자료형을 매크로의 파라미터로 사용할 수 있도록 수정했다.

다음은 이 아이디어를 구현한 코드이다.

locker 클래스는 주소값에 대응하는 값(이 코드에서는 int형 key를 사용했지만, 윈도우의 크리티컬 섹션을 위해서는 CRITICAL_SECTION 타입의 변수가 될 것이다)을 등록/삭제/검색 한다.

class locker
{
public:
    locker()
    : _key(0)
    {}

    ~locker()
    {
        map<unsigned int, int>::iterator    itr, itrend;
        itr     = _users.begin();
        itrend  = _users.end();
        for (; itr != itrend; ++itr)
        {
            unregister_addr((void*)itr->first);
        }
    }

    int register_addr(void* address)
    {
        map<unsigned int, int>::iterator    itr = _users.find((unsigned int)address);
        if (itr != _users.end())    return itr->second;

        cout    << "register: " << hex << (unsigned int)address << endl;
        _users[(unsigned int)address]   = ++_key;

        return _key - 1;
    }

    int unregister_addr(void* address)
    {
        int key = 0;
        map<unsigned int, int>::iterator    itr = _users.find((unsigned int)address);
        if (itr == _users.end())    return key;

        key = itr->second;

        cout    << "unregister: " << hex << (unsigned int)address << endl;
        _users.erase((unsigned int)address);

        return key;
    }

    int key(void* addr)
    {
        map<unsigned int, int>::iterator    itr = _users.find((unsigned int)addr);
        if (itr == _users.end())    return 0;

        return itr->second;
    }

private:
    map<unsigned int, int>  _users;
    int             _key;
};// class locker

이 클래스에는 약간의 문제가 있는데, _users, _key 변수 역시 크리티컬 섹션이라는 점이다. 따라서 이에 대한 처리도 필요하다. 또한, 주소값에 대한 key를 동적으로 생성하는데 대한 성능상의 문제도 존재한다. 그렇지만, 어디까지나 이는 아이디어를 구현하기 위한 과정으로 판단해서 일단 패스. :)

scope_lock_args 클래스는 CSBLOCK 매크로가 여러 개의 인자를 받을 수 있도록 해준다.

class scope_lock_args
{
friend class scope_lock;
public:
    scope_lock_args(int count, ...)
    {
        va_list addresses;
        va_start(addresses, count);

        for (int i = 0; i < count; ++i)
            _addresses.insert(va_arg(addresses, void*));

        va_end(addresses);
    }

    typedef std::set<void*> data_type;
private:
    std::set<void*> _addresses;
};// class scope_lock_args

scope_lock 클래스의 생성자와 소멸자에서 크리티컬 섹션의 시작과 종료를 알린다. 각 데이터는 scoke_lock_args 클래스가 주소에 따라 정렬해서 관리하기 때문에 항상 같은 순서로 락을 걸고 풀 수 있다.

class scope_lock
{
public:
    scope_lock(scope_lock_args arg)
    : _arg(arg)
    {
        scope_lock_args::data_type::iterator    itr, itrend;
        itr     = _arg._addresses.begin();
        itrend  = _arg._addresses.end();
        for (; itr != itrend; ++itr)
        {
            _locker.register_addr(*itr);
            cout    << "scope_lock::scope_lock " << hex << *itr << endl;
            // EnterCriticalSection
        }
    }

    ~scope_lock()
    {
        scope_lock_args::data_type::reverse_iterator    itr, itrend;
        itr     = _arg._addresses.rbegin();
        itrend  = _arg._addresses.rend();
        for (; itr != itrend; ++itr)
        {
            cout    << "scope_lock::~scope_lock " << hex << *itr << endl;
            // LeaveCritialSection
        }
    }

    operator bool() const
    {
        return true;
    }

private:
    static locker   _locker;
    scope_lock_args _arg;
};// class scope_lock

locker  scope_lock::_locker;

하일라이트. CSBLOCK는 최소 하나부터 최대 세 개까지의 데이터를 인자로 받을 수 있다. 물론, 필요하면 늘릴 수 있다.

#define CSBLOCK(...)    CSBLOCK_IMPL(__VA_ARGS__, 3, 2, 1)
#define CSBLOCK_IMPL(_1, _2, _3, N, ...)    if (scope_lock __sl = scope_lock_args(N, _1, _2, _3))

이제는 CSBLOCK 매크로에 여러 개의 인자를 넣을 수 있고, 그 순서에 상관없이 항상 같은 순서로 락을 사용한다.

#include <iostream>
#include <map>
#include <set>
#include <cstdarg>

using namespace std;

int main()
{
    int k   = 0;
    int l   = 0;

    CSBLOCK(&k)
    {
        cout    << "<1>" << endl;
        CSBLOCK(&l, &k)  // 순서 상관 없음
        {
            cout    << "<2>" << endl;
        }

        CSBLOCK(&k)
        {
            cout    << "<3>" << endl;
        }

        CSBLOCK(&k, &l)  // 순서 상관 없음
        {
            cout    << "<4>" << endl;
        }
    }

    return 0;
}

성능상의 문제로 실제로 사용하기에는 많은 개선이 필요하겠지만, 꽤 매력적이지 않나? ㅋㅋㅋ
C++은 확실히, 이런 것들이 재미있는 것 같다.

Critical Section Block

다음은 멀티쓰레드 프로그래밍에서 부지런한 프로그래머가 흔히 사용하는 코드의 일부이다. 

알다시피 이와 같은 방법은 코드의 수정을 복잡하게 만들고, 데드락을 피하기 위해서 락의 잠금과 해제 쌍을 완벽히 유지해야 하는 부담도 가지고 있다. Exception이나 갑작스런 흐름의 변화는 이러한 부담을 가중시킨다. 결정적으로…, 귀찮다.

그래서 조금 더 게으른 프로그래머는 auto_ptr을 흉내 낸 클래스를 만들어 사용한다.

크리티컬섹션의 범위가 좁은 경우에는 – 그렇다! 보호해야 하는 것은 데이터이지 함수가 아니다 – AutoCriticalSection타입의 변수의 범위를 제한해 주어야 한다.

위 코드는, 그것을 아름답지 못하게 만드는 두 가지 문제점 있다. 첫 번째는, AutoCriticalSection의 변수 명을 사용자가 선택해야 한다는 것이고, 두 번째는 블럭이 의미하는 바를 직관적으로 알아차리기 어렵다는 것이다.

아래는 이를 해결하기위한, 좀 더 많이 게으른 프로그래머의 코드이다.

두 가지 문제점을 개선한 이 코드에는 두 가지 아이디어가 녹아있다.

첫 번째는, 가독성있는 블록을 사용하기 위해서는 기존의 문법에서 방법을 빌린 것이다. 처음에는 이를 위해서 for문을 사용했다. 그러나 loop를 한번만 돌게 하기 위해서 CriticalSectionContainer클래스에 추가적인 변수가 필요했고, 블록 내에서 break와 continue를 사용할 수 있는 어색함이 남게 된다. 사소한 문제지만, 이는 변수 하나에 해당하는 양의 메모리와 한번의 점프 오퍼레이션이 소모된다. 그래서 if문을 사용했고, if문에서 참, 거짓을 판단하기 위해서 bool 연산자 오버로딩을 사용한 것이 두 번째 아이디어다.

다음은 이를 어떻게 사용하는지 보여준다.

블록이 의미하는 바가 무엇인지 분명히 나타나며, 사용자는 새로운 변수에 대해서 고민하지 않아도 되며, 이를 위해서 희생해야 하는 성능상의 손실은 if문 내에서 발생하는 한 번의 비교뿐이다.

부라보~