본문 바로가기

Game Server

[C++/서버] Event

결론 : 이벤트라는 커널 오브젝트를 이용해서, 만약 스레드가 함수를 진행하기 위해서 어떤 상황이 필요한 경우 이 상황이 발생할 때까지 기다리게 해서 CPU의 사용량을 낮출 수 있는 프로그래밍 기법이다.

 

  1. 화장실 문이 열릴 때까지 대기
  2. 일단 자리로, 그리고 나중에 다시 랜덤하게 돌아오기
  3. 화장실 문 앞에서 대기할 직원 하나 두고 난 일자리로 돌아간 뒤, 일이 끝나면 직원이 나한테 알려주기

이제 마지막 방식을 알아보자. 직원은 사실 커널 오브젝트이다. 커널 오브젝트는 이 락이 풀리는 '이벤트'가 끝날 때까지 대기를 했다가 이 이벤트가 끝난 경우 그 자원을 기다리던 프로그램에 알려주는 식이다.

 

이벤트는 정말 간단하다. 화장실 문이 열린다와 닫힌다 이 두 상황이 있는 것이다. 화장실 안에는 사람이 있고 밖에는 기다리는 사람이 있는 것이다. 그리고 식당의 웨이터는 화장실에서 사람이 나오면 알려주는 식이다. 먼저 화장실에 사람 있음이 뜨고 있다면 사람은 열심히 밥을 먹고 있고, 그 화장실에서 사람이 나온다면 웨이터는 대기하던 사람한테 가서 화장실이 비었어요라고 하는 식인 것이다.

 

즉 컴퓨터도 마찬가지로 어떤 스레드가 락을 잡고 있다면, 다른 스레드는 Blocking된 상태 즉 대기 상태로 기다리고 있다가, 기존 스레드가 이 락을 놓는다면 이 이벤트 커널 오브젝트가 가서 깨워주면 대기 상태에 빠져있던 프로그램이 락을 잡고 그 자원을 사용한 뒤 나가는 식인 것이다.

 

다음과 같은 코드가 있다고 가정하자.

#include "pch.h"
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <Windows.h>

using namespace std;
using namespace FrokEngine;

mutex m;
queue<int32> q;

void Producer()
{
	while (true)
	{
		{
			unique_lock<mutex> lock(m);
			q.push(100);
		}
		this_thread::sleep_for(100ms);
	}
}

void Consumer()
{
	while (true)
	{
		{
			unique_lock<mutex> lock(m);
			if (!q.empty())
			{
				int32 data = q.front();
				q.pop();
				cout << data << endl;
			}
		}
	}
}

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

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

	return 0;
}

잘 작동은 한다.

하지만 아쉬운 건 계속 데이터가 들어오는 데이터가 아니라 가아끔 데이터가 들어오는 경우, Consumer 입장에서는 계속 while(true)를 돌면서 데이터가 있는지 확인한다. 즉 CPU를 계속 낭비하는 잉여 작업을 하게 된다는 뜻이다. 

디버그 모드를 통해서 알 수 있는 CPU 점유, 계속 낭비중이란 걸 알 수 있다.

이러한 CPU의 낭비를 방지하기 위해서 데이터가 들어온 이벤트가 발생한 경우에만 실행하겠끔 바꿔보자. 먼저 windows.h에서 제공하는 이벤트 함수에 대해 알아보자.

// 1번째 인자 : 보안 요소(nullptr로 두면 OS가 자동으로 지정한다.)
// 2번째 인자 : AUTO? MANUAL?
// 3번째 인자 : 초기 상태(시그널? 논시그널?)
// 4번째 인자 : 이름
HANDLE hEvent = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);

// 사용하던 핸들을 삭제
::CloseHandle(hEvent);

커널 오브젝트는 정말 커널에서 사용하는 오브젝트이다. 그리고 이 커널 오브젝트는 다음과 같은 요소를 가진다.

  1. Usage Count
  2. Signal (bool)

그리고 이벤트는 커널 오브젝트는 상대적으로 가벼운 편이다. 자 이제 데이터가 들어온 경우 이벤트가 Signal 상태로 변화하게 만들어보자.

void Producer()
{
	while (true)
	{
		{
			unique_lock<mutex> lock(m);
			q.push(100);
		}

		::SetEvent(hEvent);
		this_thread::sleep_for(100000s);
	}
}

그리고 이벤트가 발생하기 전까지 무한 대기하게 만들려면서 만약 이벤트 상태가 Signal 상태가 되면 실행하겠끔 변화를 시킨.

void Consumer()
{
	while (true)
	{
		::WaitForSingleObject(hEvent, INFINITE);

		{
			unique_lock<mutex> lock(m);
			if (!q.empty())
			{
				int32 data = q.front();
				q.pop();
				cout << data << endl;
			}
		}
	}
}

코드는 많이 추가되진 않았지만 코드가 돌아가는 방식은 완전히 달라졌다.

 

이제 AUTO/MANUAL RESET이 무엇인지 알아보자.

AUTO는 WaitForSingleObject 상태에서 잠들다가 이벤트 객체가 Signal 상태가 된 경우 커널이 Consumer 쪽 스레드를 복원시켜서 일을 시키고, Non-Signal 상태로 자동으로 변화시켜준다.

즉 MANUAL 상태면 추가적으로 Non-Signal상태로 바꿔주는 과정이 필요하다.

::ResetEvent(hEvent);

자 다시 디버깅 상태에서 CPU의 점유를 살펴보자.

특히 이 방법의 경우는 멀티스레드 프로그래밍 뿐만 아니라 멀티 프로세스 등등에서도 응용할 수 있는 방식이다.

 

단점이라면, 결국 제3자 즉 커널까지 개입을 해서 추가적인 비용이 필요하기 때문에, 상황에 따라서 악수가 될 수 있다.

'Game Server' 카테고리의 다른 글

[C++/서버] Future  (0) 2022.07.12
[C++/서버] Condition Variable  (0) 2022.07.12
[C++/서버] Sleep  (0) 2022.07.12
[C++/서버] Spinlock  (0) 2022.07.07
[C++/서버] Lock-2  (0) 2022.07.07