멀티쓰레드 환경에서 동기화를 위해 사용하는 방법중 하나로 '특정 조건이나 이벤트가 발생할 때까지 대기하게 한다는 점'에서Event와 동작 방식이 비슷하다고 할 수 있다.
하지만 Event는 Lock에 대한 관리를 수동으로 해주어야한다는 단점이 있다. 아래 코드는 [C++ 서버] Event에서 다룬 Producer-Consumer 예제이다.
mutex m;
queue<int32> q;
HANDLE handle;
void Producer() {
while (true) {
{
unique_lock<mutex> lock(m);
q.push(100);
}
::SetEvent(handle);
}
}
void Consumer() {
while (true) {
::WaitForSingleObject(handle, INFINITE);
unique_lock<mutex> lock(m);
if (q.empty() == false)
{
int32 data = q.front();
q.pop();
cout << data << endl;
}
}
}
위 예제에서 동기화를 위해 Event를 사용했는데 Consumer에서 쓰레드가 대기상태로부터 깨어나고 Queue에 대한 Lock을 잡기 전에 다른 쓰레드가 공유자원인 Queue에 접근하지 않는다는 보장이 있을까? 정답은 아니다. 당장 위의 예제만 보아도 Producer는 SetEvent()를 하고 지속적으로 Lock과 push를 반복한다.
이러한 문제가 발생한 이유 중 하나는 '쓰레드가 대기상태에서 벗어나는 단계'와 '공유자원에 대한 Lock을 얻는 단계'가 분리되어 있기 때문이다. Producer에서 Consumer 쓰레드를 깨운 시점에 Consumer가 Lock을 얻기 위해서, condition_variable를 사용할 수 있다.
mutex m;
queue<int32> q;
HANDLE handle;
condition_variable cv;
void Producer() {
while (true) {
{
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; });
// if (q.empty() == false)
{
int32 data = q.front();
q.pop();
cout << q.size() << endl;
}
}
}
Event는 Windows API의 일부이고, condistion_variable은 C++ 표준 라이브러리임을 주의하자.
wait종류 함수의 경우 인자로 unique_lock를 받는다. 조건변수를 이용한 동기화 방법은 대기상태에서 실행상태로 전환될 때 전달받은 unique_lock에 대한 lock을 얻기 때문에 이 과정에서 사용할 Lock 객체를 받는다.
추가로, 특정 조건을 매개변수로 입력할 수 있다. 특정 조건이 의미하는 것은 실행 상태에 있을 때 Lock을 얻기 위한 조건으로 true면 Lock을 얻고, false면 다시 대기 상태로 돌아간다.
Producer의 동작순서는 기존에 Event를 사용하는 방식과 동일하다.
하지만, Consumer는 다르게 동작한다.
'Consumer가 여러 쓰레드가 존재한다면 전부 Lock에 잡혀 CPU자원을 낭비하는것이 아닐까?'라는 생각이 들 수 있다. 가장 중요한 것은 Lock을 잡은 후 얼마나 빠른 시간내에 Lock을 반환하는 것인데, 조건이 false인 경우 매우 빠른시간 내에 Lock을 반환할 것이고 true인 경우에는 형성된 임계 구역이 얼마나 빠른 시간에 처리되는 지에 따라 달라질 것이다.
Producer에서 Queue에 데이터를 notify를 한다면, Consumer에서 Queue에 데이터가 있다고 확신할 수 있고 결국 바로 Lock을 얻은 후 Queue에 접근하면 되지 않을까? 결론부터 말하자면 그렇지 않다. Consumer에서 Lock을 잡기 전에 다른 쓰레드에서 Queue의 데이터를 변경할 수도 있기 때문이다. 이런 상황을 'Spurious wakeup'라고 한다. 문자 그대로 '이상한 기상'인 이 현상은 Producer에서 notify와 Consumer의 Lock 사이에 다른 누군가가 개입할 수 있는 상황에서 발생 가능한 현상이다.
이런 현상을 완화하기 위한 방법중 하나로 위에서 언급한 특정 조건에 대한 명시를 하는 것이다. Lock을 얻고 공유자원에 대한 조건을 확인함으로 공유자원의 상태에 대한 확신을 하고 코드를 진행할 수 있다.