Condition Variable
Event의 발전된 버전이다
Event 보단 Condition Variable을 거의 대부분 사용한다
mutex랑 짝을 지어 사용하는 방법을 알아볼 것이다
그 외에 condition_variable_any를 사용하는 방법도 있다
#include <mutex>
conditon_variable cv;
#include <condition_variable>
condition_variable_any cva;
Event vs Condition Variable
condition_variable은
Event처럼 커널 오브젝트가 아닌 유저(레벨) 오브젝트이다
커널 오브젝트는 여러 프로그램에서 사용 가능하고
유저 오브젝트는 하나의 프로그램 내에서만 사용 가능하다는 특징이 있다
참고로, 게임서버는 여러 프로그램에서 교류를 하지 않기에
딱히 해당 특성을 이용하진 않는다
Condition Variable 예시
#include <mutex>
mutex m;
queue<int32> q;
condition_variable cv;
void Producer()
{
while (true)
{
// 1. Lock 잠그기
// 2. 공유 변수 값을 수정
// 3. Lock 풀기
// 4. ConditionVariable(조건변수)를 통해서 다른 쓰레드에게 통지
{
unique_lock<mutex> lock(m);
q.push(100);
}
cv.notify_one();
}
}
void Consumer()
{
while (true)
{
unique_lock<mutex> lock(m);
cv.wait(lock, []() { return q.empty() == false; });
// 1. Lock 잠그기
// 2. 조건 확인
// - 조건 만족 O: 빠져 나와서 이어서 코드를 진행
// - 조건 만족 X: Lock을 풀어주고 대기 상태
//if (q.empty() == false)// 굳이 필요 없어짐
{
int32 data = q.front();
q.pop();
cout << q.size() << endl;
}
}
}
int main()
{
std::thread t1(Producer);
std::thread t2(Consumer);
t1.join();
t2.join();
}
사실 Event 사용법과 그렇게 다르진 않다
condition_variable을 선언하고
CV를 켜줄 때, notify_one을 이용해 wait 중인 스레드가 있으면, 딱 1개만 깨운다
그리고 wait 함수를 통해 조건을 확인해서 대기 or 진행을 한다
Condition Variable의 notify_one 주의사항
위의 코드에서 본 것처럼 사용할 때 주의사항이 있다
notify_one을 이용해 이벤트를 켜주기 전에는
반드시 Lock을 잠궈서 공유 변수를 수정하고 Lock을 풀어주는 작업이 선행돼야 한다
물론, Lock을 풀어주지 않더라도 사실 wait에서 강제로 Lock을 잠그기 때문에
큰 의미는 없지만 4단계 방식으로 CV를 활용하는 것이 추천된다
void Producer()
{
while (true)
{
{
unique_lock<mutex> lock(m);// 1. Lock 잠그기
q.push(100);// 2. 공유 변수 값을 수정
}// 3. Lock 풀기
cv.notify_one();// 4. ConditionVariable(조건변수)를 통해서 다른 쓰레드에게 통지
}
}
Condition Variable의 wait 내부동작
wait의 내부 동작을 주의해서 사용해야한다
wait 함수가 실행되면,
일단 먼저 Lock을 잠근다
그리고 조건을 확인해서
조건이 참이면, 아래의 코드로 진행하고
조건이 거짓이면, 강제로 잠근 Lock을 풀고 대기상태에 접어든다
참고로, 조건이 거짓이면 Lock을 풀어야 하기 때문에
wait의 첫 번째 인자는 lock_guard 타입을 받아주지 않는다
lock_guard는 생성자와 소멸자에 lock과 unlock이 자동으로 되기 때문이다
따라서, unlock을 수동으로도 가능한 unique_lock 타입을 받는 것이다
또한, wait의 내부에서 lock을 강제로 잠그지만
우리는 아래의 코드에서처럼 wait를 호출하기 이전에 수동으로 잠그는 부분을 진행했다
이처럼 Event와는 다르게
CV의 wait를 통해서 강제로 lock을 걸고 조건에 따라서
lock을 풀고 대기할지, 아래의 코드를 진행할지 달라진다는 점이 핵심이다
void Consumer()
{
while (true)
{
unique_lock<mutex> lock(m);
cv.wait(lock, []() { return q.empty() == false; });
// 1. Lock 잠그기
// 2. 조건 확인
// - 조건 만족 O: 빠져 나와서 이어서 코드를 진행
// - 조건 만족 X: Lock을 풀어주고 대기 상태
//if (q.empty() == false)// 굳이 필요 없어짐
{
int32 data = q.front();
q.pop();
cout << q.size() << endl;
}
}
}
Spurious Wakeup
사실 CV notify가 호출되면 이미 공유변수 작업이 끝났는데
굳이 CV wait의 조건문처럼 조건을 굳이 확인해야 하나 싶을 수 있다
하지만,
운영체제 최적화(ex. 특정 시스템 이벤트나 가상 메모리 관리의 부작용)나
여러 스레드가 동일한 조건 변수를 공유할 때, 신호가 전달되지 않았음에도
깨어나는 Spurious WakeUp 상황이 발생할 수 있다
따라서,
CV wait의 조건문을 통해 안정성 및 효율성을 높일 수 있다
'게임 서버' 카테고리의 다른 글
[게임서버 섹션2 Note] 캐시 (0) | 2024.12.30 |
---|---|
[게임서버 섹션2 Note] Future (0) | 2024.12.30 |
[게임서버 섹션2 Note] Event (0) | 2024.12.30 |
[게임서버 섹션2 Note] Sleep (0) | 2024.12.29 |
[게임서버 섹션2 Note] SpinLock (0) | 2024.12.29 |