vector<int32> v; // 공유 데이터가 컨테이너
void Push()
{
for (int32 i = 0; i < 10000; i++)
{
v.push_back(1);
}
}
int main()
{
std::thread t1(Push);
std::thread t2(Push);
t1.join();
t2.join();
cout << v.size() << endl;
}
20000이 출력되어야 할 것 같지만, 실제로는 크래시가 발생한다.
vector
의 경우 계속 push하면서 size가 늘다가 capacity를 넘어가면 capacity를 늘리기 위해, 늘어난 capacity만큼 동적할당을 하고 원래 있던 원소를 복사한 후 기존에 할당된 메모리를 해제한다.
참고: [C++] vector container 정리 및 사용법
step1
[1][2][3][4]
step2
[1][2][3][4]
[1][2][3][4][][][][]
step3
[1][2][3][4][][][][]
t1
에서 capacity를 늘리기 위해 step3까지 가서 기존의 메모리가 해제되었을 때 t2
도 동일하게 capacity를 늘리기 위해 step1에 진입하면 이미 해제된 메모리를 참조하면서 크래시가 발생한 것이다. (=double-free)
int main()
{
v.reserve(20000);
std::thread t1(Push);
std::thread t2(Push);
t1.join();
t2.join();
cout << v.size() << endl;
}
20000이 출력되어야 할 것 같지만, 그렇지 않다.
[1][2][3][4][][][][]
이는 한 쓰레드가 이미 4개의 원소가 있음을 알고 index=4에 5를 push할 때 다른 쓰레드도 동시에 index=4에 5를 push하는 경우가 있기 때문이다.
앞서 언급한 문제들을 해결하기 위해서는 Lock을 사용한다.
(지난 시간에 배웠던 Atomic은 vector에 한 쓰레드만 접근할 수 있도록 하는 기능을 제공하지 않는다.)
#include <mutex>
mutex m;
void Push()
{
for (int32 i = 0; i < 10000; i++)
{
m.lock(); // 다른 쓰레드는 접근 불가
v.push_back(1);
m.unlock(); // 다른 쓰레드도 접근 가능
}
}
mutex는 일종의 자물쇠라고 생각하면 된다.
lock을 하면 자물쇠를 걸어 다른 쓰레드는 접근이 불가능하다.
따라서 다른 쓰레드가 m.lock()
에 도달하면 lock을 한 쓰레드가 unlock하기 전까지는 기다리게 된다.
따라서 실제로 for문의 내부 코드는 싱글쓰레드로 동작하게 되며 capacity를 미리 할당하지 않더라도, 정상적으로 20000이 출력되는 것을 볼 수 있다.
int main()
{
std::thread t1(Push);
std::thread t2(Push);
t1.join();
t2.join();
cout << v.size() << endl;
}
Lock의 특징 : Mutex Exclusive(상호배타적)
쓰레드 A가 lock을 획득하면 다른 쓰레드는 접근할 수 없다.
void Push()
{
for (int32 i = 0; i < 10000; i++)
{
m.lock();
v.push_back(1);
if (i == 5000)
{
// unlock하지 않고 for문을 빠져나옴
break;
}
m.unlock();
}
}
Lock을 수동으로 제어한다면 다음과 같이 unlock을 하지 않아 다른 쓰레드가 영원히 lock을 기다리는 상황이 발생할 수 있다.
이를 방지하기 위해 RAII패턴을 사용한다.
// RAII
template<typename T>
class LockGuard
{
public:
LockGuard(T& m)
{
_mutex = &m;
_mutex->lock();
}
~LockGuard()
{
_mutex->unlock();
}
private:
T* _mutex;
};
RAII는 생성자에서 자원을 할당하고 소멸자에서 자원을 해제한다.
void Push()
{
for (int32 i = 0; i < 10000; i++)
{
LockGuard<std::mutex> lockGuard(m);
v.push_back(1);
if (i == 5000)
{
break;
}
}
}
이렇게 RAII 패턴을 사용한 LockGuard 객체를 사용한다면 수동으로 lock을 제어하지 않아도, for문이 한 번 실행될 때마다 unlock을 호출한다.
std::lock_guard<std::mutex> lockGuard(m);
void Push()
{
for (int32 i = 0; i < 10000; i++)
{
std::unique_lock<std::mutex> uniqueLock(m, std::defer_lock);
uniqueLock.lock();
v.push_back(1);
if (i == 5000)
{
break;
}
}
}
객체를 생성할 때 lock을 걸지 않고 나중에 원하는 타이밍에 걸 수 있다. 물론 unlock은 uniqueLock
객체가 소멸될 때 호출된다.
코드
#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <atomic>
#include <mutex>
vector<int32> v;
mutex m;
// RAII
template<typename T>
class LockGuard
{
public:
LockGuard(T& m)
{
_mutex = &m;
_mutex->lock();
}
~LockGuard()
{
_mutex->unlock();
}
private:
T* _mutex;
};
void Push()
{
for (int32 i = 0; i < 10000; i++)
{
std::unique_lock<std::mutex> uniqueLock(m, std::defer_lock);
uniqueLock.lock();
v.push_back(1);
if (i == 5000)
{
break;
}
}
}
int main()
{
std::thread t1(Push);
std::thread t2(Push);
t1.join();
t2.join();
cout << v.size() << endl;
}