다음과 같은 헤더 파일들이 추가되어 있습니다.
#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <atomic>
#include <mutex>
#include <windows.h>
condition_variable은 C++ 표준 라이브러리에서 제공하는 동기화 메커니즘으로, 특정 조건을 만족할 때까지 스레드를 대기 상태로 둘 수 있습니다. 이를 통해 스레드 간의 작업 순서를 보장하고, 자원 경쟁 및 동시성 문제를 해결할 수 있습니다.
먼저 Producer와 Consumer라는 두 개의 스레드 함수를 살펴봅시다.
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; });
{
int32 data = q.front();
q.pop();
cout << q.size() << '\n';
}
}
}
Producer 함수는 무한 루프를 돌면서 q라는 큐에 값을 넣습니다. 이 때, unique_lock lock(m);를 사용하여 동시 접근을 막습니다. 그리고 cv.notify_one();을 통해 대기 중인 스레드가 있으면 하나를 깨웁니다.
Consumer 함수는 무한 루프를 돌면서 q라는 큐에서 값을 가져옵니다. 이 때, cv.wait(lock, { return q.empty() == false; });를 통해 큐에 값이 들어올 때까지 대기하게 됩니다.
cv.notify_one();을 사용할 때는 lock을 잡고 있지 않기 때문에 Spurious Wakeup이 발생할 수 있습니다. 이는 컴퓨터의 하드웨어나 운영 체제의 특성 때문에 스레드가 깨어나기도 하는데, 이를 방지하기 위해 cv.wait() 함수에서 조건을 체크합니다.
cv.wait(lock, { return q.empty() == false; }); 코드에서는 큐가 비어있지 않을 때까지 스레드를 대기 상태로 둡니다. 이렇게 하면, 큐에 값이 추가되어 cv.notify_one();에 의해 깨어난 스레드가 실제로 처리해야 할 작업이 있는 경우에만 작업을 수행하게 됩니다.
다음은 이들 함수를 main() 함수에서 실행하는 코드입니다.
int main()
{
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
}
Producer와 Consumer 함수를 각각 다른 스레드에서 실행시킵니다. 이 때, join() 함수를 사용하여 Producer와 Consumer 스레드가 모두 종료될 때까지 메인 스레드의 실행을 블록합니다.
condition_variable과 Event는 모두 스레드 동기화에 사용되는 메커니즘입니다. 하지만 사용 방식과 원리에는 몇 가지 중요한 차이점이 있습니다.
Condition Variable: :condition_variable은 스레드가 특정 조건이 충족될 때까지 대기하게 하는 동기화 메커니즘입니다. 스레드는 wait 함수를 호출하여 특정 조건이 참이 될 때까지 대기 상태로 전환될 수 있습니다. 다른 스레드에서 notify_one 또는 notify_all을 호출하면 대기 중인 스레드 중 하나 또는 모든 스레드가 깨어나게 됩니다. condition_variable은 직접적으로 "signal / non-signal" 상태를 가지지 않으며, 조건 검사는 사용자 코드에서 명시적으로 수행해야 합니다.
Event (Windows): Event는 Windows에서 제공하는 동기화 객체로, 두 가지 상태인 "signaled" 상태와 "non-signaled" 상태를 가집니다. 스레드는 이벤트가 signaled 상태가 될 때까지 대기하게 됩니다. 이벤트는 자동 리셋 이벤트와 수동 리셋 이벤트 두 가지 유형이 있습니다. 자동 리셋 이벤트는 signaled 상태로 설정된 후 첫 번째 대기 스레드를 깨우고 바로 non-signaled 상태로 돌아갑니다. 수동 리셋 이벤트는 수동으로 리셋할 때까지 signaled 상태를 유지합니다.
Spurious wakeup 혹은 가짜 기상은 condition_variable을 사용할 때 발생할 수 있는 현상입니다. 이는 스레드가 아무런 notify 없이 wait 함수에서 반환되는 경우를 말합니다. 이러한 현상은 시스템 성능 최적화나 스레드 간의 경쟁 상황 등에 의해 발생할 수 있습니다.
가짜 기상은 condition_variable의 wait 함수가 항상 조건이 참인 경우에만 반환된다는 보장이 없다는 점에서 나옵니다. 따라서 대기 조건을 검사하는 루프를 사용하여 wait 함수를 호출하면 가짜 기상에 대응할 수 있습니다. 이를 통해 스레드가 wait 함수에서 반환할 때 조건이 충족되지 않은 경우 다시 대기 상태로 돌아가게 할 수 있습니다.
cv.wait(lock, []() { return q.empty() == false; });
위 코드에서 cv.wait 함수의 두 번째 인자로 주어진 람다 함수가 바로 조건을 검사하는 부분입니다. wait 함수가 반환된 후에도 이 조건이 거짓인 경우에는 다시 wait 함수를 호출하여 스레드를 대기 상태로 만듭니다. 이렇게 하면 가짜 기상에 대비할 수 있습니다.
즉, std::condition_variable의 wait 메서드는 두 개의 인자를 받는데, 첫 번째는 std::unique_lock 객체, 두 번째는 조건을 검사하는 함수입니다. 이 함수가 false를 반환하면 wait 함수는 계속해서 스레드를 대기 상태로 만듭니다. 이 함수가 true를 반환할 때에만 wait 함수가 스레드를 깨우고 반환합니다. 이렇게 하면 notify_one이 호출되었을 때 조건이 만족되지 않더라도 스레드가 계속 대기 상태로 있을 수 있게 됩니다. 이는 가짜 기상을 방지하고, 스레드가 필요한 작업이 준비될 때까지 대기 상태로 있도록 하는 데 도움이 됩니다.
이런 특성으로 인해 condition_variable을 사용하면 다양한 복잡한 동기화 문제를 해결할 수 있습니다. 하지만 잘못 사용하면 교착 상태(deadlock)와 같은 동시성 문제를 일으킬 수 있으므로 주의가 필요합니다.