본문 바로가기
게임 서버/[Inflearn_rookiss]올인원_클라&서버 연동

7. 이벤트와 조건변수

by 헛둘이 2023. 12. 11.

이벤트

- 이벤트도 동기화 기법 중 하나로, 스핀락과 비교해보면 스핀락은 자리가 날 때까지 무한히 대기하는 것이고, 이벤트는 승무원에게 자리가 나면 불러달라고 하고 자러 가는 것
- 예제에서의 이벤트는 Windows API를 사용하는 예제이며, 커널오브젝트를 사용한다
- 커널 오브젝트는 Usage count와 Signal State(Signal / Non-Signal)을 가진다.
- 먼저 아래는 스핀락을 이용한 Producer / Consumer 패턴

#include <iostream>
#include <Windows.h>
#include <mutex>
#include <queue>
using namespace std;

mutex m1;
mutex m2;
HANDLE hEvent;
queue<int> q;


void Producer()
{
	while (true)
	{
		lock_guard<mutex> guard(m1);
		q.push(100);

		this_thread::sleep_for(100ms);
	}
}

void Consumer()
{
	while (true)
	{
		lock_guard<mutex> guard(m2);
		if (!q.empty())
		{
			int value = q.front();
			q.pop();
			cout << value << endl;
		}
	}
}

int main()
{
	thread t1(Producer);
	thread t2(Consumer);

	t1.join();
	t2.join();

	return 0;
}

- 여전히 잘 작동하지만, 스핀락은 자리가 날때까지 무한히 시도하므로 Task가 빨리 마무리되지 않는 부분에선 적절치 않다.
 
 


아래는 이벤트를 사용한 Producer / Consumer 패턴
Producer / Consumer 패턴은 이벤트를 사용하는 것이 효율적이다.
 

#include <iostream>
#include <Windows.h>
#include <mutex>
#include <queue>
using namespace std;

mutex m1;
mutex m2;
HANDLE hEvent;
queue<int> q;


void Producer()
{
	while (true)
	{
		lock_guard<mutex> guard(m1);
		q.push(100);

		SetEvent(hEvent);
		this_thread::sleep_for(100ms);
	}
}

void Consumer()
{
	while (true)
	{
		WaitForSingleObject(hEvent, INFINITE);
		lock_guard<mutex> guard(m2);
		if (!q.empty())
		{
			int value = q.front();
			q.pop();
			cout << value << endl;
		}
	}
}

int main()
{
	hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

	thread t1(Producer);
	thread t2(Consumer);

	t1.join();
	t2.join();

	CloseHandle(hEvent);

	return 0;
}

- Producer에서 큐에 자료가 들어가면 SetEvent를 통해 시그널을 켜주고, Consumer에서는 WaitForSingleObject로 대기하다가 (계속 시도하는 것이 아님) Event가 Signal 상태가 되었을 때 해당 업무를 처리한다.
- 그리고 주 스레드에서는 프로그램이 종료되면 CloseHandle로 참조계수를 감소시켜 hEvent를 정리한다
 
 


조건변수(Condition Variable)

- 조건변수는 이벤트와 유사한데, C++ 표준에 추가되어 좀 더 사용하기 편하다
- Producer에서 cv.notify_one()은 현재 대기중인 스레드 중 하나를 깨우는 것이고, notify_all()은 전체를 깨운다
- Consumer에서 wait을 사용하는데 wait에서는 락 하나와 조건을 받는다
- 락은 아래 서술할 내용이지만 스코프 중간에 잡았다 놓았다 하기 때문에 lock_guard로는 안되고, unique_lock을 사용해야 한다.
- 조건은 간단하게 람다로 작성 가능하다.
 

#include <iostream>
#include <Windows.h>
#include <mutex>
#include <queue>
using namespace std;

mutex m1;
mutex m2;
queue<int> q;
condition_variable cv;

void Producer()
{
	while (true)
	{
		lock_guard<mutex> guard(m1);
		q.push(100);

		cv.notify_one();
		// 하나만 깨운다.
		this_thread::sleep_for(100ms);
	}
}

void Consumer()
{
	while (true)
	{
		unique_lock<mutex> guard(m2); // unique_lock은 중간에도 락을 걸거나 풀 수 있으므로 cv와 사용 가능하다
		cv.wait(guard, []() { return !q.empty(); });

		// 1. Lock이 잡혔는지 확인
		// 2. 조건 확인
		// ㄴ 조건 만족 : 아래 내용 진행
		// ㄴ 조건 불만족 : 다시 Sleep

		//if (!q.empty())
		{
			int value = q.front();
			q.pop();
			cout << value << endl;
		}
	}
}

int main()
{
	thread t1(Producer);
	thread t2(Consumer);

	t1.join();
	t2.join();

	return 0;
}

- Consumer에서 하는 작업이 좀 특이한데,
- 처음에 Lock이 잡혔는지 확인한 후 람다식으로 받은 조건을 체크한다.
- 조건을 만족하면 아래 코드를 실행하고, 만족하지 않는다면 다시 Sleep에 들어간다.

'게임 서버 > [Inflearn_rookiss]올인원_클라&서버 연동' 카테고리의 다른 글

9. 소켓 프로그래밍  (0) 2023.12.11
8. 스마트 포인터  (2) 2023.12.11
6. 데드 락  (0) 2023.12.11
5. 스핀 락  (0) 2023.12.11
4. Lock 기초  (0) 2023.12.11

댓글