KoreanFoodie's Study

[C++ 게임 서버] 1-9. 이벤트 구현 (+ Handle) 본문

Game Dev/Game Server

[C++ 게임 서버] 1-9. 이벤트 구현 (+ Handle)

GoldGiver 2023. 7. 14. 19:29

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 1-9. 이벤트 구현 (+ Handle)

핵심 :

1. 이벤트를 사용하면 SpinLock 에 비해 CPU 낭비를 줄일 수 있는 장점이 있다.

2. 이벤트의 개념은 간단하다. 핸들을 만든 후, Consumer 는 핸들을 참고하여 깨울 때까지 준비 상태에 들어가고, Producer 가 준비가 되었을 때 핸들에게 이벤트를 보내기만 하면 된다.

3. 핸들은 커널 오브젝트에 해당하여, 여러 쓰레드가 이를 활용할 수 있다.

이제 Lock 구현을 함에 있어 '무작정 기다리기' 와 '잠깐 기다리기' 를 구현해 보았으니, 마지막 순서가 남아있다.

바로... '제 3자가 깨워주기' 인데, 여기서 '제 3자'라 함은 일반적으로 OS 의 스케줄러를 의미하고, '깨워준다' 는 것은 해당 쓰레드에 시그널을 보내 작업을 시작한다고 이해하면 된다. 🤣

 

멀티쓰레드 하면 국밥처럼 등장하는, Producer - Consumer 코드를 '잠깐 기다리기' 방법으로 구현한 아래 코드를 보자.

using namespace std;

mutex m;
queue<int> q;

void Producer()
{
	for (int i = 0; i < 10000; ++i)
	{
		{
			unique_lock<mutex> lock(m);
			q.push(i);
		}

		this_thread::sleep_for(10000ms);
	}
}

void Consumer()
{
	while (true)
	{
		unique_lock<mutex> lock(m);

		if (!q.empty())
		{
			int data = q.front();
			q.pop();
			cout << "Data : " << data << endl;
		}
	}
}

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

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

	cout << "main() exit" << endl;
}

아래 코드를 보면, Producer 는 일정 시간 간격으로 Queue 에 데이터를 넣고, Consumer 는 계속해서 대기하며 q 에 데이터가 있으면 pop 을 한다.

이 코드는 의도대로 동작하긴 하지만, Comsumer 의 경우 Producer 가 Queue 에 데이터를 언제 넣는지 모르므로, 계속 대기하며 데이터가 있는지를 체크한다. 이 과정에서 CPU 사용의 낭비가 발생하게 되는데, 이벤트를 사용해 Producer 가 Queue 에 데이터를 넣었을 때 Consumer 에게 알려줘서 Consumer 를 깨워주면, 그런 낭비를 없앨 수 있을 것이다!

 

그럼 이제 Event 를 사용한 코드를 보자. 😁

using namespace std;

mutex m;
queue<int> q;

HANDLE handle;

void Producer()
{
	for (int i = 0; i < 10000; ++i)
	{
		{
			unique_lock<mutex> lock(m);
			q.push(i);
		}

		// 이제 핸들에 이벤트를 전달한다
		::SetEvent(handle);

		this_thread::sleep_for(100ms);
	}
}

void Consumer()
{
	while (true)
	{
		// 시그널이 올 때까지 잠들어 있는다. INFINITE 라서 시그널 올때까지 무한정...
		::WaitForSingleObject(handle, INFINITE);

		// 만약 HANDLE 에서 bManualReset 이 TRUE 이면 매번 수동으로 이벤트를 리셋해줘야 함
		//::ResetEvent(handle);

		unique_lock<mutex> lock(m);

		if (!q.empty())
		{
			int data = q.front();
			q.pop();
			cout << "Data : " << data << endl;
		}
	}
}

int main()
{
	// 커널 오브젝트
	// Usage Count
	// Signal (파란불) / Non-Signal (빨간불) << bool
	// Auto / Manual << bool

	handle = ::CreateEvent(NULL/*보안속성*/, FALSE/*bManualReset*/, FALSE/*bInitialState*/, NULL/*Name*/);

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

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

	::CloseHandle(handle);

	cout << "main() exit" << endl;
}

중요한 건, 커널 오브젝트에 해당하는 Handle 을 만들어서 Producer 가 Queue 에 데이터를 넣었을 때, 핸들에게 이벤트를 보낸다는 것이다.

Consumer 의 경우, 핸들을 참고하며 이벤트가 올때까지 대기 (여기서의 대기는 CPU 자원을 소모하는 대기가 아니라 잠드는 것에 가깝다) 하게 된다.

실제로, 이벤트를 사용하여 구현하면 이전의 방식처럼 무한 루프를 도는 것보다 CPU 리소스를 적게 사용하는 것을 확인할 수 있다! 😄

각 함수의 인자에 대한 자세한 내용은 너무 신경쓰지 않아도 된다 😂 일단 개념만 잡자!

Comments