사실 event 라는거 자체가 lock 에서만 응용해서 구현해야 되는 방법은 아니고,

굉장히 사용 범위가 높다.

 

lock과 더불어서 event를 활용해서 쓰레드를 동기화 하는게 많이 쓰이니깐 중요하다.

 

 

event 는 갑질 메타 이다. ( 직원이 중재해주는것 = 관리자 )

 

유저 레벨에서 쓰레드들의 순서가 보장 안되니깐 관리자를 두어 순서를 보장하는것.

 

C# 기준으로는 AUTO RESET EVENT 랑 MANUAL RESET EVENT  두 가지 클래스로 구분이 되어 있었는데

c++ 에서는 인자를 뭐로 주는가에 따라 AUTO RESET EVENT가 될 수도 있고, MANUAL RESET EVENT가 될 수 도 있다.

 

EVENT는 쉽게 말해서 Bool 값이라고 생각하면 된다. ( 문이 열린다, 문이 잠긴다  정도로 )

 


 

어떤 한명이 화장실을 들어갈려고 하는데 사람이 있어서 식당 관리자한테 화장실 알림판이 사용가능으로 변하면 나를 꺠워줘 하고 잠들게 된다 . 그러면 화장실을 이용하고 있던 사람이 화장실을 빠져나가면서 자물쇠를 풀어줄때 사용가능으로 알림판을 바꾸고 식당 관리자는 그것을 보고 잠자는 사람을 깨우게 된다.

 

장점이 뭔가 생각해보면 spin lock 처럼 계속 기다리는것이 아니기에 쓰레드 입장에서는 효율적으로 행동 할 수 있으나

제 3자 ( 커널모드 )의 리소스를 사용하는것이기 때문에 비용이 든다.

그래서 이 갑질 메타를 남발하기보다는 어느정도 대기가 확정적이라면 갑질 메타를 쓰는것이 좋다.

 

#include <iostream>
#include <Windows.h>
#include<mutex>
#include<queue>

using namespace std;
using int32 = __int32;

mutex m;
queue<int32> q;  // 공용 데이터를 관리하는 queue

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

void Consumer() {
	while (1) {
		unique_lock<mutex> guard(m);
		
		if (!q.empty()) {
			int32 data = q.front();
			q.pop();
			// lock을 잡고 콘솔 출력하는게 좋은 습관은 아니다.
			// 간단하게 테스트를 하기위해서 
			cout << data << endl;
		}
	}
}

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

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

 

실행해보면 100ms 마다 100 , 100 , 100 ... 이 출력되는것을 알 수 있다.

딱히 문제있는것은 아닌데, 한가지 아쉬운 점은 

 

sleep_for(100) 이면 그나마 0.1초마다 한번씩 queue에 무엇을 넣기 때문에 그나마 consumer에서 데이터를 가져가는데 sleep_for(100000000ms) 처럼 producer 가 진짜 어쩌다가 큐에 데이터를 넣으면

 

받는 consumer가 while 문 돌면서 계속 try 하면서 체크하게 되면서, cpu 점유율만 높아지고 비효율적이다.

 

 

 

그래서 뭔가 패킷이 올때만 알려주세요~ 라는 event ( 갑질 메타 ) 방식을 사용한다.


 

 

#include <window.h> 에 있는 window api를 사용할것이고 코드는 ::CreateEvent()이다.

받는 인자는 4개가 있다. ( 그 중에서 2번째 , 3번째 인자가 중요하다. )

 

첫번째 인자는 보안 속성과 관련있는거라 지금은 신경쓸 필요 없이 NULL 로 넣어주고

 

두번째 인자는 Manual Reset이라고 해서 bool 값을 받는데 true 면 수동으로 reset이 되게끔 하고

false 면 auto-reset 방식의 event로 작동할것이다.

 

세번째 인자는 initialState라고 해서 Event의 초기 상태를 받아주게 된다.

- True / false 

 

네번째 인자는 이름을 정해주는건데 필수적인것은 아니라 NULL로 넣어주기로 하자.

 

#include <iostream>
#include <Windows.h>
#include<mutex>
#include<queue>

using namespace std;
using int32 = __int32;

mutex m;
queue<int32> q;  // 공용 데이터를 관리하는 queue
HANDLE handle;

void Producer() {
	while (1) {
		{
			unique_lock<mutex> guard(m);
			q.push(100);
		}
		// 커널 오브젝트의 상태를 시그널 상태(파랑불)로 바꿔주세요~~ 
		::SetEvent(handle);

		this_thread::sleep_for(100ms);
	}
}

void Consumer() {
	while (1) {

		// 무한대기 하게끔 하는게 아니라
		// 커널 오브젝트가 시그널(파랑불) 상태면 계속 진행할것이고,
		// 빨강불( Non-signal) 상태면 쓰레드가 일어나지 않고 잠들게 될것이다.
		// 화장실 예로 들자면 쓰레드가 자리로 가서 자고 있다가 커널한테 갑질 하는것이다.
		::WaitForSingleObject(handle, INFINITE /*대기시간(-1)*/);

		unique_lock<mutex> guard(m);
		
		if (!q.empty()) {
			int32 data = q.front();
			q.pop();
			// lock을 잡고 콘솔 출력하는게 좋은 습관은 아니다.
			// 간단하게 테스트를 하기위해서 일단은 사용하자.
			cout << data << endl;
		}
	}
}

int main() {
	// CreateEvent 라는 window api를 사용할것이고 
	// 받아주는 인자는 4개가 있다. 

	// 커널 오브젝트 : 커널에서 사용하는 오브젝트다.
	handle = ::CreateEvent(NULL/*보안속성*/, FALSE, FALSE, NULL);
    // 두번째 인자 bManualReset을 false로 주면 auto(자동으로) 상태를 변화게 한다는거로
    // waitforsingleobject에서 커널 오브젝트가 시그널상태로 변한걸 감지하고 멈춰있던 쓰레드를 다시
    // 깨워서 동작하게 함과 동시에 singal 상태로 진입했으니깐 다시 non-singal 상태로 바꿔준다.

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

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


}

 

CreateEvent() 를 호출해주면 얘는 어디까지나 컨텐츠단( 유저레벨)에서 마음대로 처리할 수 있는것은 아니고

Event 라는것 자체가 커널레벨로 가서 커널에서 만들어주는 아이 이기 떄문에 실질적으로 Event를 커널 오브젝트라고도 한다.

그래서 얘가 뱉어주는것(return) 해주는것을 보면 HANDLE 이라는것을 리턴해주는데 handle은 실질적으로 우리가 사용하는 Int 랑 마찬가지로 정수에 불과하다. ( 실제로 F12 타고 가보면 void* 이다. )

10,20,30 등 일정의 번호표라고 생각하면 된다. ( 이벤트 식별자라고 생각하면 편하다.)

 ( 이벤트가 하나가 아니라 여러개일텐데 특정 Event를 이용해서 작업하고 싶을때 해당 handle을 전달해줘서 식별할 수 있게끔.  )

 

그리고 우리가 메모리를 delete하느것과 마찬가지로 ::CloseHandle(handle) 로 닫아주는게 좋은 습관이다.

 

커널 오브젝트라고 했는데 말장난 같지만 정말 이게 다 이다. 커널에서 사용하는 오브젝트이다.

이벤트 뿐만 다양한 커널에서 관리하는 객체를 만들어줄떄 커널 오브젝트라는게 생성되는데 그냥 커널에서 관리하는 오브젝트이다. 커널에서 이 커널 오브젝트라는것을 관리하기 위해서 할당되는 일련의 메모리가 있는데 그것을 커널 오브젝트라고 생각해주면 된다.

 

기본적으로 커널 오브젝트에서 공통적으로 가지고 있는 속성이 여러가지가있는데

Usage Count : 이 오브젝트를 몇명이 사용하고 있는지

Signal (파란불) / Non-Signal (빨간불) << bool : 관리하기 위해 내부적으로 bool 변수를 두어, 두 가지 상태를 가지고 있다. 

 

위 두가지는 커널 오브젝트가 공통적으로 가지고 있는거고

 

우리는 커널 오브젝트 중에서도 event 를 다루고 있으니깐

자동 모드 (Auto ) / 인지 수동 (Manual) 으로 꺼지는 모드 인지 를 나타내는 bool 을 가지고 있다. 

 

커널 오브젝트 중에서 이벤트는 상대적으로 가벼운 편이다. ( 부담없이 사용할 수 있다.)

 

두번째 인자/*bManualReset*/ 를 false로 했기 때문에 Consumer에서 ::WaitForSingleObject(handle,INFINITE) 하고 수동으로 다시 ::ResetEvent(handle) 안해줘도 되는거다.

True 로 했으면 ::ResetEvent(handle)을 해줬어야 됐을 것이다. 

 

 

 

 

실제로 위 코드를 다시 실행시켜보면 

 

정상적으로 작동하고 CPU 점유율이 아까와 다르게 0% 인것을 알 수 있다. 

( waitForSingleObject 쓰레드는 커널오브젝트가 signal로 바뀌기 전까지는 blocked 되어서 대기하면서 실행조차 되지 않고 쿨쿨 잠들고 있는 것이다. ) 

singal 상태로 변하면 그때서야, 다시 스케줄링 상태로 넣어주는것을 알 수 있다.

 

 

,  

커널 오브젝트( 이벤트, 등등 ) 같은 객체를 통해 동기화를 시켜주게 되면은

지금은 하나의 프로그램 내부에서만 동기화를 해주고 있는데 다른 프로그램과의 동기화 작업도 가능하다. ( 물론 mmo 서버에서는 다른 프로그램이랑 동기화 할 상황이 없긴하다. )

 

그래서 결론은

 

 spin_lock 같은 동기화 방법은 User Level 에서 일어나는 동기화  기법이였다고 한다면, 

커널 오브젝트를 통한 동기화는 Kenerl Level 에서 일어난 동기화라 활용성이 좋을 수도 있지만, 

 

멀리 있는 커널까지 리소스를 쓰기 때문에 빈번하게 이용하게 된다면 악수가 될 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

+ Recent posts