Mutex

  • atomic은 일반적인 상황에서 쓰이기 힘들다.
  • 객체에 대한 원자성을 보장해야 하는 일이 훨씬 많기 때문

Mutex

문제 발생 코드

vector<int> v;

void Push()
{
    for(int i = 0 ; i < 10000 ; i++)
    {
        v.push_back(i);
    }
}
int main()
{
    std::thread t1(Push);
    std::thread t2(Push);
    
    t1.join();
    t2.join();
    
    cout << v.size() << endl;
    return 0;
}

실행 결과

오류 발생
  • 오류 발생 이유 : vector는 크기가 2^n에 도달했을때 기존 capacity의 두배가 되는 공간을 새로 할당받고 기존 공간을 없애는데, 원자성 보장이 되지 않기 때문에, 이미 없어진 공간을 다시 없애려 하기 때문.
  • v.reserve(20000)으로 미리 20000크기의 공간을 할당받으면 오류는 사라지지만, 여전히 원자성은 보장이 되지 않아 결과가 20000이 나오지 않음.

Mutex Lock

vector<int> v;
mutex m;

void Push()
{
    for(int i = 0 ; i < 10000 ; i++)
    {
        m.lock();
        v.push_back(i);
        m.unlock();
    }
}
int main()
{
    std::thread t1(Push);
    std::thread t2(Push);
    
    t1.join();
    t2.join();
    
    cout << v.size() << endl;
    return 0;
}

실행 결과

20000
  • mutex lock을 통해 상호배제 달성
  • 속도는 기존보다 많이 느려짐.
  • mutex lock은 재귀적으로 걸 수 없음.
    • m.lock(); m.lock();을 연속으로 하면 crash발생.
    • 재귀적 lock을 허용하는 recursive mutex에 관해서는 추후 학습.

RAII(Resource acquisition is initialization) 패턴


vector<int> v;
mutex m;


template<typename T>
class LockGuard
{
private:
    T* _mutex;
public:
    LockGuard(T& m)
    {
        _mutex = &m;
        _mutex -> lock();
    }
    ~LockGuard()
    {
        -mutex -> unlock();
    }
}


void Push()
{
    for(int i = 0 ; i < 10000 ; i++)
    {
        LockGuard<std::mutex> lockGuard(m);
        v.push_back(i);
        if(i == 5000) break;
    }
}
int main()
{
    std::thread t1(Push);
    std::thread t2(Push);
    
    t1.join();
    t2.join();
    
    cout << v.size() << endl;
    return 0;
}
  • 래퍼 클래스를 통해 레이어(레벨) 종료시 인스턴스가 스택에서 해제되는 원리를 이용하여 unlock()을 따로 명시하지 않고도 소멸자를 통해 자동으로 unlock()가능.
  • lock_guard, unique_lock 등의 표준이 있음`
    • lock_guard는 위의 예시와 동일하게 사용
    • unique_lock은 추가 기능이 더 있음.
      • unique_lock<mutex> uniqueLock(m, defer_lock)과 같이 defer_lock 옵션을 주면 명시적으로 lock을 하는 타이밍을 정할 수 있음. uniqueLock.lock()과 같이 사용.