동기화 메커니즘 중 하나도 Event를 사용할 수 있는데 [C# 서버] Event에서 Event를 이용한 동기화 방법에 대해서 다뤄본적이 있다. C#과 다른점이라면 C#은 AutoResetEvent, ManualResetEvent로 따로 생성해줄 수 있었지만 C++에는 파라미터로 지정해주어야 한다.
아래 코드는 두 개의 쓰레드가 공유자원인 Queue에 각각 push, pop하는 구조를 가지는 코드이다. 두 함수는 Queue에 접근하기 전에 mutex를 이용해 Queue 다른 쓰레드가 접근하는 것을 방지하고 연산을 진행한다.
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() == false)
{
int32 data = q.front();
q.pop();
cout << data << endl;
}
}
}
int main() {
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
return 0;
}
위 코드에서 만약 Producer가 100ms가 아니라 100초, 1000초마다 push한다면? Consumer는 100초동안 계속 확인하면서 CPU자원을 낭비하게 될 것이다.
Event로 이 문제를 해결할 수 있다. Producer는 push Event의 Signal을 활성화시켜 Consumer 쓰레드에게 알리는 방법
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(100ms);
}
}
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);
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
::CloseHandle(handle);
return 0;
}
SpinLock은 유저단계에서 일어나는 동기화 기법이라면 Event는 커널단계까지 개입하는 방법이라고 할 수 있다. 결국 커널모드로의 전환이 이루어지기 때문에 Lock과정에서 이뤄지는 작업이 짧은 단위로 끝난다면 SpinLock이 더 효울적일수도 있다. 결국, 상황에 따라 맞는 방법을 선택하는 것이 가장 중요하다고 생각한다.
Event를 만들고, 기다리고, 상태를 변경함으로 쓰레드간의 동기화가 가능하다. Event를 다루는 대표적인 방법은 아래와 같다.
CreateEvent로 Event를 생성할 수 있고, 식별하기 위한 Handle을 반환한다.SetEvent로 Signal을 활성화 시킬 수 있고, ResetEvent로 비활성화 시킬 수 있다.WaitForSingleObject, WaitForMultipleObjects로 Event가 Signal될 때까지 대기상대로 들어갈 수 있다. 함수 이름을 보면 하나를 기다리거나 여러개를 기다릴 수 있다.CloseHandle로 생성된 Handle을 닫음으로 커널 오브젝트를 해제할 수 있다.Event를 생성할 때 반환값으로 Handle을 반환한다. Handle은 Signal을 활성화 시킬 때 Handle을 같이 넘겨줌으로 커널에서 어떤 Event를 활성화시킬지 식별할 수 있도록 해준다.
AutoResetEvent는 Wait에서 깨어난 후에 자동으로 Evnet를 Reset시켜주고 하나의 쓰레드만 깨어나는 것을 보장할 수 있다. 하지만 ManualResetEvnet는 수동으로 Reset 함수를 호출해주어야 된다. 즉, Event가 Signal 상태가 되었을 때 깨우는 쓰레드의 개수에 차이가 있다.