#include <iostream>
#include <thread>
#include <mutex>
#include <Windows.h>
mutex m;
queue<int> q;
HANDLE handle;
void Producer()
{
while (true)
{
{
unique_lock<mutex> lock(m);
q.push(10);
}
SetEvent(handle); // 이벤트의 상태가 Signal로 바뀜
this_thread::sleep_for(100ms);
}
}
void Consumer()
{
while (true)
{
//Non-Signal일 때 Sleep
WaitForSingleObject(handle, INFINITE);
//ManualReset이 FALSE이므로 자동으로 Non-Signal로 돌아옴
unique_lock<mutex> lock(m);
if (q.empty() == false)
{
int data = q.front();
q.pop();
cout << q.size() << endl;
}
}
}
int main()
{
// 커널 오브젝트
// Usage Count
// Auto / Manual << bool
// Signal / Non-Signal << bool
handle = CreateEvent(NULL/*보안속성*/, FALSE/*bManualReset*/, FALSE/*bInitialState*/, NULL);
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
CloseHandle(handle);
}
전 게시글의 코드를 가져와서 출력만 큐의 사이즈로 바꿔보았다. 단순히 생각해보면 Producer가 생산을 하고 0.1초간 대기를 할 때 Consumer가 깨어나서 소비를 하게 되므로 큐의 사이즈는 0이 된다고 생각할 수 있다. 하지만 코드를 실행해보면 큐의 사이즈가 계속 늘어난다는 것을 알 수 있다.
이러한 결과가 나타나는 이유도 결국 락을 획득하는 코드와 이벤트의 상태를 변경하고 확인하는 코드가 분리되어 있기 때문이다. Consumer가 이벤트의 상태를 확인하고 깨어난 후 락을 획득하는데 텀이 존재하고 그 시점에 Producer가 데이터를 생산할 수 있기 때문이다.
#include <iostream>
#include <thread>
#include <mutex>
#include <Windows.h>
mutex m;
queue<int> q;
// User-Level Object(커널 오브젝트 X)
condition_variable cv;
void Producer()
{
while (true)
{
// 1) Lock 획득
// 2) 공유 변수 수정
// 3) Lock 해제
// 4) 조건 변수를 통해 다른 스레드에게 통지
{
unique_lock<mutex> lock(m);
q.push(10);
}
cv.notify_one(); // 대기중인 스레드 중 1개만 깨움
this_thread::sleep_for(100ms);
}
}
void Consumer()
{
while (true)
{
unique_lock<mutex> lock(m);
cv.wait(lock, []() { return q.empty() == false; });
// 1) Lock 획득
// 2) 조건 확인
// - 만족 O -> 빠져나와 코드 진행
// - 만족 X -> Lock 해제 후 대기
int data = q.front();
q.pop();
cout << q.size() << endl;
}
}
int main()
{
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
}
Condition Variable은 이벤트와 거의 비슷하게 동작한다. 다만 Condition Variable은 유저 레벨 오브젝트로 커널 모드인 이벤트와는 차이점이 있다. Producer는 cv(Condition Variable)을 통해 다른 스레드들에게 상태를 알려준다. notify_one()은 대기중인 스레드 1개만 notify_all()은 모든 스레드에게 상태를 통지하는 것이다. Consumer는 wait()를 호출하게 되고 Lock과 조건을 인자로 받는다. 위에서는 람다식으로 조건을 전달해주었다.
notify를 해서 깨어났으면 반드시 조건을 만족하는 것이 아닌가 하는 의문이 생길 수 있다. 하지만 Producer가 Lock을 해제하고난 후 통지를 하게 되고 다른 스레드들도 깨어나면서 바로 Lock을 획득하는 것이 아니기 때문에 그 사이 텀에 다른 스레드가 공유 데이터를 수정하게 되면 조건을 만족하지 못하는 상황이 발생할 수 있다. 시그널을 받고 깨어났지만 조건을 만족하지 못하는 상황이 되어버린 것이다. 이러한 상황을 spurious wakeup, 직역하면 가짜 기상이라고 한다. 따라서 wait()에서 조건을 통해 크로스체킹 한다고 보면 되겠다.
Event과 비슷하게 동작하지만 조금 더 효율적으로 동작한다. 옛날 방식인 Event보다 Condition Variable를 애용하도록 하자
'서버 > 멀티스레드' 카테고리의 다른 글
10. 메모리 모델 (0) | 2024.07.24 |
---|---|
9. Future (0) | 2024.07.21 |
7. Event (0) | 2024.07.20 |
6. Sleep (0) | 2024.07.17 |
5. SpinLock (1) | 2024.07.14 |