서버/멀티스레드

8. Condition Variable

광란의슈가슈가룬 2024. 7. 20. 13:57
#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