본문 바로가기
프로그래밍/C++

C++ ] mutex, lock_guard

by eteo 2023. 10. 8.

 

 

mutex

 

mutex(뮤텍스)란 다중 스레드 환경에서 공유 데이터에 대한 동시 접근을 제어하기 위해 사용하는 동기화 기법 중 하나이다. mutual exclusion의 약자로 여러 스레드가 동시에 접근하지 못하도록 lock 하는 기능을 제공한다.

 

 

 

mutex 사용 예시

 

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex myMutex;
int sharedVariable = 0;

void incrementSharedVariableWithMutex(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        myMutex.lock(); // 뮤텍스를 잠근다.
        // 공유 데이터에 대한 안전한 업데이트를 수행한다.
        ++sharedVariable;
        myMutex.unlock(); // 작업 완료 후 뮤텍스를 해제한다.
    }
}

int main() {
    const int numIterations = 1000000;
    
    thread thread1(incrementSharedVariableWithMutex, numIterations);
    thread thread2(incrementSharedVariableWithMutex, numIterations);

    thread1.join();
    thread2.join();

    cout << "Final sharedVariable value: " << sharedVariable << endl;

    return 0;
}

 

 

 

lock_guard

 

lock_guard는 C++ 표준 라이브러리에서 제공하는 클래스중 하나로 뮤텍스 관리에 대한 실수를 줄일 수 있도록 해준다. lock_guard를 사용하면 뮤텍스를 자동으로 잠그고 해당 범위(scope)가 벗어하면 lock_guard의 소멸자가 호출되어 자동으로 뮤텍스를 해제한다.

 

 

 

lock_guard 사용법

 

#include <iostream>
#include <mutex>

using namespace std;

mutex myMutex;

void someFunction() {
    lock_guard<mutex> lock(myMutex);
    // 뮤텍스가 잠겨 있을 때 이 코드 블록 안의 작업을 수행한다.
    // 뮤텍스가 벗어나면 자동으로 뮤텍스가 해제된다.
    // 다른 스레드는 이 뮤텍스를 획득하려고 시도하면 블록된다.
}

int main() {
    // 여러 스레드에서 someFunction을 동시에 호출해도 뮤텍스 덕분에
    // 한 번에 하나의 스레드만 someFunction 내의 코드를 실행할 수 있다.
    
    return 0;
}

 

 

 

 

 

lock_guard 사용 예시

 

 

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex myMutex;
int sharedVariable = 0;

void incrementSharedVariable(int iterations) {
    lock_guard<mutex> lock(myMutex);
    for (int i = 0; i < iterations; ++i) {
        ++sharedVariable;
    }
}

int main() {
    const int numIterations = 1000000;
    
    thread thread1(incrementSharedVariable, numIterations);
    thread thread2(incrementSharedVariable, numIterations);

    thread1.join();
    thread2.join();

    cout << "Final sharedVariable value: " << sharedVariable << endl;

    return 0;
}

 

 

뮤텍스는 주로 위 예시와 같이 공유 데이터의 안전한 업데이트를 보장하기 위해 사용된다. 

 

스레드의 numIterations이 1000000이고 두 스레드에서 sharedVariable을 증가하고 있으니 최종 값이 2000000이 나와야 옳지만 뮤텍스를 사용하지 않으면 두 스레드가 동시에 sharedVariable을 업데이트하면서 잘못된 결과가 나올 수 있다.

 

 

 

처음에는 스레드 안에서 cout을 사용해 출력이 엉키는 경우가 있는데 테스트 해보려고 했는데 cout 스트림 자체는 내부적으로 스레드간 동기화가 되어 있어서 동시에 여러 스레드가 cout을 사용해도 출력이 엉키지 않았다.

 

 

 

 

lock_guard<mutex>의 적용 범위

 

위의 코드에선 한 스레드가 sharedVariable을 1000000번 증가시키는 동안 다른 스레드는 myMutex 뮤텍스에 대한 잠금을 기다려야 한다. 만약 두 스레드가 각각 독립적으로 1000000번 증가시키는게 아니라 번갈아가며 증가시키도록 하면 어떨까?

 

void incrementSharedVariable(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        lock_guard<mutex> lock(myMutex);
        ++sharedVariable;
    }
}

 

lock_guard<mutex>는 블록 범위에 락을 적용하는데 만약 lock_guard<std::mutex> lock(myMutex);를 for문 안쪽에 배치한다면 매 반복마다 뮤텍스를 잠그고 해제하기 때문에 lock_guard의 생성자와 소멸자가 매번 호출하면서 오버헤드가 발생하여 성능이 저하될 수 있다. 때문에 작업의 성격과 공유 자원에 대한 접근 패턴을 고려해서 lock_guard를 적용할 스코프를 선택해야 한다.