Mutex(뮤텍스)란?
mutex란 Data race를 해결하는 가장 기초적인 방법이다. mutex는 Data race뿐 아니라 Race condition까지 해결해준다.
Race condition
공유되는 메모리 공간과는 상관 없이 thread가 어떠한 순서로 엮이느냐에 따라 결과값이 달라질 때 발생한다.
Example Code
#include <iostream>
#include <thread>
#include <string>
#include <chrono>
void printThreadID()
{
std::cout << "threadID: " << std::this_thread::get_id() << std::endl;
}
int main()
{
std::thread t1(printThreadID);
std::thread t2(printThreadID);
t1.join();
t2.join();
return 0;
}


위와 같이 2개의 thread에서 thread 아이디를 출력하는 코드를 계속해서 실행해보면 어떠한 순간에는 각각의 ID가 섞여서 출력이 될 때가 있다. 이것이 바로 Race condition 때문에 일어난 것이다. t1이 t2보다 먼저 실행되는 경우, t2가 t1보다 먼저 실행되는 경우, t1과 t2과 동시에 실행되어 섞이는 경우와 같이 thread의 타이밍에 따라서 결과값이 달라지는 것이 레이스 컨디션이다.
mutex 사용
뮤텍스의 기본동작에는 lock과 unlock이 있다. 여러 thread들 중에 보호하고싶은 부분에 mutex lock을 걸어주면 mutex lock을 획득한 오직 하나의 thread만 접근이 가능하다.
Example Code
#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <mutex>
int num = 0;
std::mutex mtx;
void plus10000()
{
for (int i = 0; i < 10000; i++)
{
mtx.lock();
num++;
mtx.unlock();
}
}
int main()
{
std::thread t1(plus10000);
std::thread t2(plus10000);
t1.join();
t2.join();
std::cout << "num: " << num << std::endl;
return 0;
}
num을 증가시키기전에 mutex lock을 걸고 num을 증가시킨 뒤 mutex lock을 해제시킨다면 num이 증가되는 부분은 오직 하나의 thread만 접근이 가능하게 되면서 데이터 레이스를 방지할 수 있다.
메모리 접근

처음 접근한 thread를 t2라고 가정하였을 때,
- t2는 mutex lock을 획득해서 num 값을 0에서 1로 바꾸는 연산을 수행한다. 이 공간은 mutex에 의해 보호받고 있다.
- 이 때 t1이 접근하여 mutex를 획득하려고 하면 thread 진행이 block이 되면서 기다리게 된다.
- 먼저 들어온 t2가 동작을 마치고 mutex unlock을 하게되면 block이 되어있던 thread들 중 하나가 다시 mutex lock을 획득하면서 연산을 진행한다.
이렇게 mutex로 하나의 thread만 접근할 수 있도록 만들어진 지역을 Critical Section(크리티컬 섹션)이라고 한다. mutex를 이용해서 하나의 크리티컬 섹션을 지정할 때 주의해야할 점이 있는데 이 크리티컬 섹션을 최소화해야한다는 것이다.
만약 위 코드에서 lock을 건 코드 구간이 짧지 않으며 훨씬 많은 내용이 있고, 그 많은 내용 중에 thread간의 공유가 되는 데이터가 없다고 가정했을 때, 모든 부분을 mutex로 보호하면 안된다. 굳이 보호하지 않아도 되는 부분을 오직 하나의 thread만 진행할 수 있기 때문에 비효율적인 프로그램이 된다. 즉, 크리티컬 섹션은 범위를 최소화해야만 효율적인 프로그램을 작성할 수 있다.
Example Code
또한, 착각하는 것 중 하나가 있는데 Block 상태에 있던 thread들 중 mutex를 다시 얻을 때에는 순서가 없다는 것이다.
#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <mutex>
#include <vector>
int num = 0;
std::mutex mtx;
void plus10000()
{
mtx.lock();
for (int i = 0; i < 10000; i++)
{
num++;
}
mtx.unlock();
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++)
{
threads.push_back(std::thread(plus10000));
}
for (auto& t : threads)
{
t.join();
}
std::cout << "num: " << num << std::endl;
return 0;
}
위 코드는 thread 10개를 생성하여 num을 10000씩 증가시키는 동작을 수행한다. 그리고 num을 10000번 증가시켜주는 부분을 크리티컬 섹션으로 지정하였다.
메모리 접근

가장 첫번째 들어온 thread가 mutex lock을 획득하고 연산을 수행한다. 그 와중에 다른 thread가 하나씩 도착하면서 mutex lock을 획득하려하지만 이미 lock을 획득한 thread가 있기 때문에 모두 block 상태가 된다.
이후 처음 들어온 thread가 동작을 마치고 mutex unlock을 하게 되는데 그럼 block 상태에 있던 여러 thread들 중 오직 하나의 thread만 lock을 획득해서 크리티컬 섹션으로 들어갈 수 있는데 먼저 와서 기다린 thread가 lock을 획득한다는 보장은 없다.
즉, 기다리던 순서와 상관없이 하나의 thread만 lock을 획득할 수 있다는 의미이다.
'C++ > Thread' 카테고리의 다른 글
[Thread] std::unique_lock (0) | 2022.07.17 |
---|---|
[Thread] std::mutex (0) | 2022.07.15 |
[Thread] std::jthread (조이닝 쓰레드) (0) | 2022.07.13 |
[Thread] thread_local (0) | 2022.06.20 |
[Thread] std::thread Argument (0) | 2022.06.20 |