KoreanFoodie's Study
[C++ 게임 서버] 1-4. Lock 기초 (lock_guard, unique_lock) 본문
[C++ 게임 서버] 1-4. Lock 기초 (lock_guard, unique_lock)
GoldGiver 2023. 7. 10. 17:07
[C++ 게임 서버] 1-1. 멀티쓰레드 개론
핵심 :
1. Critical Section 에 진입할 때는 mutex 등을 활용하여 Lock 을 걸면 Race Condition 을 해소할 수 있다.
2. 일반적으로 직접 lock/unlock 을 해주기 보다, lock_guard 나 unique_lock 을 사용하여 RAII 방식으로 Lock 을 걸어 주는 것이 좋다.
3. unique_lock 의 경우, defer_lock 등의 옵션을 주어 Lock Guard 를 좀 더 유연하게 사용할 수 있다. 다만 lock_guard 에 비해 메모리를 더 많이 차지하고 무거을 것이다.
다음과 같이 여러 쓰레드를 이용해 벡터에 원소를 삽입한다고 해 보자 :
vector<int> vec;
void Push()
{
for (int i = 0; i < 10'000; ++i)
{
vec.push_back(i);
}
}
int main()
{
thread t1(Push);
thread t2(Push);
t1.join();
t2.join();
cout << "vec.size() : " << vec.size() << endl;
}
위의 코드를 실행하면, 다음과 같이 바로 크래시가 난다 :
vector 의 경우, 갯수가 늘어날 때 메모리를 새로 할당하는데 Race Condition 이 생기면 영 좋지 않은 일이 일어날 수 있을 것이다 😂
따라서 다음과 같이 vector 에 필요한 총 메모리를 미리 확보해 놓으면 크래시는 발생하지 않지만...
vec.reserve(20000);
문제는, vector 에 삽입된 원소들의 갯수가 정확히 20000 개가 되지 않게 된다!
해당 문제는 이전에 배운 atomic 으로도 해결은 되겠지만, 그건 사실 멀티쓰레드 환경에서 우리가 바라는 동작이 아닐 것이다.
이런 경우, 간단하게 mutex 를 이용해 Lock 을 걸면 문제를 해결할 수 있다!
mutex m;
void Push()
{
for (int i = 0; i < 10'000; ++i)
{
m.lock();
vec.push_back(i);
m.unlock();
}
}
위와 같이 코드를 조금 수정해 주면 된다!
다만 실제로 위와 같이 Lock 을 사용하면 문제가 생길 수 있다. 예를 들어 다음 코드를 보자.
void Push()
{
for (int i = 0; i < 10'000; ++i)
{
m.lock();
// 어떤 동작을 하다가, 실수로 unlock 을 안하고 return 을 해버린다.
if (i == 5000)
break;
vec.push_back(i);
m.unlock();
}
}
따라서 위와 같은 상황을 방지하기 위해, RAII (Resource Aquisition Is Initialization) 를 적용하여, LockGuard 를 만들어 주면 좋다 😉
template <typename T>
class LockGuard
{
public:
LockGuard(T& m)
{
_mutex = &m;
_mutex->lock();
}
~LockGuard()
{
_mutex->unlock();
}
private:
T* _mutex;
};
void Push()
{
for (int i = 0; i < 10'000; ++i)
{
// Lock 을 걸고 싶을 때 LockGuard 를 선언만 해 준다.
LockGuard<mutex> lockGuard(m);
// Lock 을 걸고 어떤 동작을 할 것이다.
vec.push_back(i);
// Scope 를 벗어나면 LockGuard 의 소멸자가 자동으로 Lock 을 푼다
// 이제 unlock 을 명시적으로 해 주지 않아도 된다.
}
}
위의 코드에서, LockGuard 를 선언만 해 주면 따로 해제할 걱정을 하지 않아도 된다!
물론 언어 표준에서 LockGuard 를 지원해 주므로, 우리가 필요할 때마다 일일히 만들 필요는 없다! 우리가 만든 LockGuard 대신 아래와 같이 Lock Guard 를 잡아주면 된다 😊
// 기본적인 LockGuard
std::lock_guard<std::mutex> lockGuard(m);
// 커스터마이징 여지가 더 많은 녀석
std::unique_lock<std::mutex> uniqueGuard(m);
위 두개의 LockGuard 는 비슷하지만, unique_lock 의 경우 옵션을 다양하게 줄 수 있고(대신 좀 더 무겁다), Copy 나 Move 가 가능하다.
예를 들어, unique_lock 은 'defer_lock' 인자를 주어 LockGuard 가 생성되는 시점과 실제 Lock 을 거는 시점을 분리할 수 있다! 😮
void foo() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// perform some operations here
lock.lock(); // acquire the lock
// critical section
lock.unlock(); // release the lock
}
unique_lock 에서 사용할 수 있는 tag 는 다음과 같다 :
tag | 의미 |
defer_lock | 뮤텍스를 잠그지 않음 (나중에 따로 잠궈야 함) |
try_to_lock | 뮤텍스를 잠그려고 시도 (이미 잠겨있으면 false 반환) |
adopt_lock | 뮤텍스가 현재 쓰레드에 의해 잠겨 있다고 가정 (잠그는 것이 아님) |
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 1-6. Lock 구현 이론 (0) | 2023.07.11 |
---|---|
[C++ 게임 서버] 1-5. DeadLock 의 개념과 기초 (+ std::lock) (0) | 2023.07.10 |
[C++ 게임 서버] 1-3. atomic (원자적) 변수와 연산 (0) | 2023.07.10 |
[C++ 게임 서버] 1-2. 쓰레드 생성 (0) | 2023.07.07 |
[C++ 게임 서버] 1-1. 멀티쓰레드 개론 (0) | 2023.07.07 |