Event
Event는 Lock에서만 활용되는 것뿐 아니라
다양하게 사용된다
앞서 Lock 구현이론에서 보았듯이 Event는
누군가에게 부탁을 해서 빈 상태가 되면 전달을 받는 방식이다
여기서 누군가는 바로 커널 영역으로
커널 영역에게 부탁을 하는 것이다
Auto Reset Event vs. Manual Reset Event
C#은 두 개의 클래스가 구분된다
C++은 인자에 따라 구분된다
둘의 차이는 이 글의 맨 마지막 부분에서 다룬다
Event 흐름
Event는 단순하게 boolean 값으로
사용 중이던 스레드가 나가면서
자물쇠를 열고 이벤트를 켜준다
그러면, 기다리는 스레드가 이벤트가 켜진 것을 확인하고 본인이 들어간다
예시코드 - Event 미사용
#include <windows.h>
mutex m;
queue<int32> q;
void Producer()
{
while (true)
{
{
unique_lock<mutex> lock(m);
q.push(100);
}
this_thread::sleep_for(1000000ms);
}
}
void Consumer()
{
while (true)
{
unique_lock<mutex> lock(m);
if (q.empty() == false)
{
int32 data = q.front();
q.pop();
cout << data << endl;
}
}
}
int main()
{
std::thread t1(Producer);
std::thread t2(Consumer);
t1.join();
t2.join();
}
위의 코드를 보면
Producer 작업이 Consumer 작업에 비해 매우 적게 실행된다는 사실을 알 수 있다
따라서, Consumer에서 반복문을 돌면서 계속 리소스가 잡아먹힌다
이러한 상황을 Event를 활용해 수정해 보자
예시코드 - Event 사용
#include <windows.h>
mutex m;
queue<int32> q;
HANDLE handle;
void Producer()
{
while (true)
{
{
unique_lock<mutex> lock(m);
q.push(100);
}
::SetEvent(handle);// 이벤트 켜줌
this_thread::sleep_for(1000000ms);
}
}
void Consumer()
{
while (true)
{
::WaitForSingleObject(handle, INFINITE);// 이벤트 기다리는 대기상태로 진입
unique_lock<mutex> lock(m);
if (q.empty() == false)
{
int32 data = q.front();
q.pop();
cout << data << endl;
}
}
}
int main()
{
handle = ::CreateEvent(NULL, FALSE, FALSE, NULL);// 이벤트 생성
std::thread t1(Producer);
std::thread t2(Consumer);
t1.join();
t2.join();
::CloseHandle(handle);// 이벤트 소멸
}
CreateEvent를 통해 이벤트를 생성하고 CloseHandle을 통해 이벤트를 삭제할 수 있다
그리고 해당 이벤트를 활용하려면
SetEvent를 통해 이벤트를 켜줄 수 있고
WaitForSingleObject를 통해 특정 시간 동안 이벤트가 켜지기를 대기하라고 할 수 있다
이는 Consumer 반복문에서 계속 돌던 이전 코드와는 다르게
스레드를 대기상태로 만들어 이벤트가 발생하는 경우에만 동작하도록 구현된 것이다
::CreateEvent 함수
해당 함수의 인자는 아래와 같다
lpEventAttributes는 보안속성을 의미한다 (추후 다루게 된다)
bManualReset은 이벤트 꺼주는 것을 수동으로 할지 자동으로 할지 설정한다
bIntialState는 초기값이다
lpName은 이벤트의 이름으로 필수는 아니며 NULL로 해도 된다
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);
커널 오브젝트
커널에서 관리되는 오브젝트를 의미한다
커널 오브젝트의 공통적인 속성은 아래와 같다
- Usage Count: 해당 오브젝트를 몇 명이 사용하고 있는지
- Signal / Non-Signal: 켜졌는지 꺼졌는지
- Auto / Manual: 자동으로 꺼지는지 수동으로 꺼지는지
Auto Reset vs. Manual Reset
Auto로 하면
이벤트가 켜질 때,
WaitForSingleObject 함수의 대기 상태가 끝나고
코드의 다음 줄로 넘어가면서
바로 Event를 Non-Signal 상태로 꺼준다
반면, Manual로 하면
ResetEvent를 통해 수동으로 꺼줘야 한다
::WaitForSingleObject(handle, INFINITE);
::ResetEvent(handle);// 이벤트 설정을 Manual로 했다면, 직접 이렇게 꺼줘야 함
Event의 오해
#include <windows.h>
mutex m;
queue<int32> q;
HANDLE handle;
void Producer()
{
while (true)
{
{
unique_lock<mutex> lock(m);
q.push(100);
}
::SetEvent(handle);// 이벤트 켜줌
}
}
void Consumer()
{
while (true)
{
::WaitForSingleObject(handle, INFINITE);// 이벤트 기다리는 대기상태로 진입
unique_lock<mutex> lock(m);
if (q.empty() == false)
{
int32 data = q.front();
q.pop();
cout << q.size() << endl;
}
}
}
int main()
{
handle = ::CreateEvent(NULL, FALSE, FALSE, NULL);// 이벤트 생성
std::thread t1(Producer);
std::thread t2(Consumer);
t1.join();
t2.join();
::CloseHandle(handle);// 이벤트 소멸
}
위의 코드를 보면, 기존 Event를 사용한 코드에서 달라진 점은 아래와 같다
- Producer 함수에서 sleep을 제거하고 계속 함수가 실행되도록 했다
- Consumer 함수에서 q.size()를 출력하도록 했다
코드를 실행해보면, q.size()가 계속해서 늘어난다
오해를 하면,
Producer와 Consumer가 한 세트씩 돌면서
q.size()가 0이 되어야할 것 같다는 생각을 할 수도 있다
하지만 그건 ㄹㅇ 오해이다
코드의 흐름을 다시 살펴보면,
Producer 함수에서 push를 하고 lock이 풀리고 이벤트를 켜준다
그리고 Consumer 함수에서 대기상태가 풀리는데
Consumer 함수의 lock 코드가 실행되기 전에
Producer가 먼저 또 빈 틈을 이용해 본인이 실행되었기 때문이다
이처럼 Event를 사용할 때
반드시 한 사이클로 작동된다는 오해는 하지 않도록 하자
'게임 서버' 카테고리의 다른 글
[게임서버 섹션2 Note] Future (0) | 2024.12.30 |
---|---|
[게임서버 섹션2 Note] Condition Variable (0) | 2024.12.30 |
[게임서버 섹션2 Note] Sleep (0) | 2024.12.29 |
[게임서버 섹션2 Note] SpinLock (0) | 2024.12.29 |
[게임서버 섹션2 Note] Lock 구현 이론 (0) | 2024.12.29 |