하아찡

mutex 본문

C++/기본문법

mutex

하아찡 2024. 12. 26. 15:13

mutex는 일반적인 상황에서는 사용이 되지않고 멀티 쓰레드를 사용하게 될때 필요합니다.

 

왜 일반적인 상황에서 사용이 안되는지부터 알아봅시다.

 

일반적으로 코드를 작성하게되면 변수값은 Main쓰레드 혼자 작업을 하게됩니다. 그러다보니 외부(다른쓰레드)에서 해당 변수값을 변경하는 일이 발생할 수 없기 때문에 사용을 하지않는데

 

만약에 우리가 다른 쓰레드를 생성해서 동일한 변수를 여러 쓰레드가 동시에 수정하게되면 어떻게 될까요?

정말 이상적으로 생각했을땐, "컴퓨터가 동시에 처리한다해도 순서라는게 있으닌깐 값이 정상적으로 변하겠지?" 라고 생각을 하실겁니다. 저도 그랬으닌깐요.

 

근데 막상 실행을 시켜보고 값을보면 정말 잘 나올 수도 있지만, 원하는 값으로 안 나올 수도 있습니다. 여기서 문제가 발생합니다.

코드를 작성했는데 내가 원하는 값으로 나와야하는데? 확률이 반반이야? 

 

왜 저렇게 결과가 반반으로 나오는지를 알아봐야합니다.

예시코드로 하나 보고가시죠.

int sum = 0;
void Add() {
    for (int i = 0; i < 1'0000; i++) {
        sum += i;
    }
}
void Sub() {
    for (int i = 0; i < 1'0000; i++) {
        sum += -i;
    }
}

int main()
{

    std::thread t1(Add);    //더한다
    std::thread t2(Sub);    //뺀다


    if (t1.joinable()) {    //쓰레드를 정상적으로 실행시킬수있는지?
        t1.join();
    }
    if (t2.joinable()) {    //쓰레드를 정상적으로 실행시킬수있는지?
        t2.join();
    }

    //값을 확인 예상값은 0
    cout << sum << endl;

}

 

Sum에게 0~9999까지 값을 더하고

Sum에게 0~9999까지 값을 빼는 함수를 실행시켰으면

당연하게도 더해진값과 빼진값이 동일하기때문에 sum은 0이라는 값이 나와야합니다.

 

하지만 결과를 실행시켜봤을때 이상한값이나옵니다.

 

읭? 왜 0이 안나올까요? 이상하죠...

 

이제 이유를 살펴보겠습니다.

우리가 함수를 쓰레드로 생성하지않고 따로따로 실행을 했을경우에는 정상적으로 0이라는 값이 나오게 됩니다.

하지만 쓰레드를 생성해서 해당함수를 실행시키면 말이 달라집니다.

서로 다른 쓰레드가 동일한 sum이라는 변수값을 가지게 되는데, 가지게 되는 시간을 봐야합니다.

 

이게 무슨소리냐

Add함수와 Sub함수가 동시에 실행이 되고 있는 상황입니다.

근데 둘이 동시에 sum값을 0인 상태로 보게됬습니다.

그래서 Add는 sum에다가 1을 더한값을 다시 sum에다 넣는 과정이 들어갔죠? 코드로 적어서보면

sum = sum(0) + 1; 이죠 하지만 여기서 둘이 동시에 sum값이 0인 상태를 봤다는게 중요합니다.

 

하지만 문제는 여기서 발생하게 됩니다.

위에서 뭐라했죠? Add함수와 Sub함수가 동시에 sum값을 확인을했을때 0이라는 값을 보게됐죠?

그러면 Sub함수도 위에서 동일하게

sum = sum(0) -1; 이라는 코드로 실행이 되게됩니다.

 

위에서 Add함수가 작동돼서 sum이 1이라는 값으로 변경이 됐는데 하필 Sub함수가 sum값을 확인했을때는 변경 전이 됩니다.

그래서 sum은 우리가 이상적으로 생각했을때 0이라는 값이 아니고 -1이라는 값이 들어가게 됩니다.

이 예시 반대로 빼고나서 더할수도있는 아이러니한 상황이 벌이지게됩니다.

 

 

이러한 연산이 누적이 되고 되면서 원하는 값을 가지지 못하게 됩니다.

 

그래서 나온게 mutex입니다.

그러면 mutex는 뭐냐? 여러개의 쓰레드가 공용변수인 값을 참조하게될때 사용권한을 한개의 쓰레드에게만 주게됩니다.

그러면 같이 접근할라했는데 다른 쓰레드는 사용권한이 없으면 어떻게되나요? -> 대기합니다. 기존에 권한 받은 쓰레드가 "나 다했어" 라고 하기전까지요.

 

자그러면 코드로 보시죠

#include <thread>
#include <atomic>
#include <mutex>
#include <iostream>
vector<int32> v;
void Test(int id, mutex& m) {
    
    for (int32 i = 0; i < 10000; i++) {
        m.lock();
        v.push_back(i);
        m.unlock();
    }
}
int main()
{
    mutex m;        //1번 자물쇠
    mutex m1;       //2번 자물쇠
    std::thread t1(Test,1, std::ref(m));
    std::thread t2(Test,2, std::ref(m));

    
    if (t1.joinable()) {
        t1.join();
    }
    if (t2.joinable()) {
        t2.join();
    }
    cout << v.size() << endl;

}

 

위코드를 보시면 Test 함수 내부에서 v벡터변수에게 값을 10000개씩 넣는 작업입니다.

 

자 그전에 변수는 값을 보는 시점에서 값을 넣으닌깐 값이 이상해질수 있는데

벡터는 그냥 마지막주소에다가 넣는거닌깐 멀티쓰레드 환경에서도 그냥 사용하면 되는거아니야? 라고 하실 수 있지만 이것도 마찬가지로 마지막주소를 동시에 같이 볼 수 있기에 정상적인 처리가 되지 않습니다.

 

그래서 mutex라는 친구로 묶어줬는데 이제 진짜 한번 살펴보시죠

mutex는 단순하게 생각합시다. 공통변수를 여러 쓰레드가 참조를 할라고 시도를 할때, 하나씩 접근이 가능하게 해주는 기능입니다.

저는 자물쇠라고 표현을 하겠습니다. 내가 접근해서 허용이 됐어! 그러면 문을 잠궈버립니다. 왜? 다른애들 못들어 오게.

대신에 문을 잠궜으면 내가 볼일을 다보면 자물쇠를 무조건 풀어줘야합니다. 안그러면 프로그램이 멈춰버리게 됩니다. 

이러한 상황을 데드락이라고 부릅니다.

 

그러면 일단 mutex가 제공 해주는것들을 보시죠

.lock() -> 접근가능하게해줘! 대신 누군가 이미 쓰고있다면 끝날때까지 숨참을게

.unlock() -> 볼일을 다봤으면 소유권을 포기할게

.try_lock() -> "야 있냐?" 있으면 false / 없으면 true 없을경우엔 .lock()과 동일한 기능을 합니다. 그러면 .lock()과 무슨차이가있냐? try_lock은 비동기입니다. 누군가 쓰고있다면 기다리지않고 다른 코드를 실행시킵니다.

#include <thread>
#include <atomic>
#include <mutex>

vector<int32> v;
void Test(int id, mutex& m) {
    
    for (int32 i = 0; i < 10000; i++) {
        if (!m.try_lock()) {    //야! 끝났냐?
            cout << "id : " << id << " 접근실패 i : " << i << endl;
            i--;
            continue;
            
        } 

        v.push_back(i);

        m.unlock();
    }
}
int main()
{
    mutex m;        //1번 자물쇠
    mutex m1;       //2번 자물쇠
    std::thread t1(Test,1, std::ref(m));
    std::thread t2(Test,2, std::ref(m));

    
    if (t1.joinable()) {
        t1.join();
    }
    if (t2.joinable()) {
        t2.join();
    }
    cout << v.size() << endl;

}

위 코드는 try_lock()을 사용했을때 입니다.

 

실행 결과

 

 

자 여기부터는 제 궁굼증이였습니다. 저도 공부를 하면서 이렇게쓰면 뭐가될까 저렇게써보면 어떻게될까 하면서 mutex도 이것저것 해봤지만 처음에 저 자물쇠라는 개념이 참 이해가 안갔습니다. 근데 너무 생각이 많았던게 독이 된 케이스 이기도하죠...?

제 궁굼증은 이거였습니다. mutex도 어차피 변수이고 경쟁순서를 정리해주는 친구인데, 그러면 그 공용데이터 접근에 대한 정보는 어떻게 처리가 되는가? 였는데,

그냥 lock() 시작 기점부터 unlock() 까지 안에 있는 공용변수를 묶어주는 역활이였더라구요. 진짜 그냥 묶어주는 역할입니다.

대신 묶여있는 애들을 참조할때 한개의 쓰레드만 접근이 가능하게 해주는 기능이고요.

 

두번째는 동일한 공동변수를 서로다른 자물쇠로잠그면 어떻게 될까였는데요?

위 경우는 서로 독립적인 상황이기때문에 정상적으로 데이터가 보호가 되지않는 상황이 발생하게 됩니다.

그래서 공통으로 사용하는 변수가 있을때는 같은 자물쇠를 사용해주세용.

 

그리고 이제 우리가 unlock을 매번 호출해줘야하는데 사람인지라 까먹을 수도 있고~ 예외처리하다보면 또 빼먹을 수 있기때문에

RAII( Resource acquisition is initialization )이란게 있습니다.

할당된것들을 소멸까지 완벽하게!

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

    ~LockGuard() {
        _mutex->unlock();
    }
};

이런식으로 작업을 할 수 있지만

std::lock_guard로 이미 존재합니다~

위 코드를 사용하시게 되면 unlock을 자동으로 해주게 됩니다.

 

좀더 자세히 알고싶다면

https://modoocode.com/270

 

씹어먹는 C ++ - <15 - 2. C++ 뮤텍스(mutex) 와 조건 변수(condition variable)>

여러 쓰레드에서 같은 객체의 값을 수정한다면 Race Condition 이 발생합니다. 이를 해결하기 위해서는 여러가지 방법이 있지만, 한 가지 방법으로 뮤텍스를 사용하는 방법이 있습니다. 뮤텍스는 한

modoocode.com

위 사이트가서 보시는것도 좋습니다!

 

공부하면서 적은 내용이라 틀린점이 있을 수 있습니다. 틀린점이 있다면 지적해주세요!!

반응형

'C++ > 기본문법' 카테고리의 다른 글

접근제한자, 캡슐화  (0) 2024.12.16
오버라이딩, 오버로딩  (1) 2024.12.15
레퍼런스(참조자) 란?  (0) 2024.12.10
변수란?  (0) 2024.12.02