Q1. AutoResetEvent, ManualResetEvent, Mutext, SpinLock의 특징과 차이를 말해 보고 어떨 사용하면 될지 설명해 보세요.
이 직원이 식당 직원이 아냐
이런 식으로 커널 레벨, 즉 아래 레벨에 있는 관리자 쪽에 있는 직원이었어.
랜덤메타보다 더 느리다 생각하면 된다.
본인 입장에서는 자물쇠 풀릴 때만 행동 이어간다는 장점이 있어.
C#에서 이벤트를 사용할 때는 2가지가 있다.
AutoResetEvent 가 있어.
톨게이트를 생각하면 된다. 하나씩 지나가는 거
물을 열고 나간 다음에 문을 안닫으면 몇대의 차든 이 문을 통과할 수 있다는 얘기가 된다.
코드로 가서 구현을 해보자.
class Lock
{
AutoResetEvent _available = new AutoResetEvent(**true**);
true면 아무나 들어올 수 있는 상태가 되는 거고, false면 들어올 수 없는 닫힌 상태가 되는 거다.
Auto가 붙었다는 건 문을 닫는 걸 자동으로 해준다는 느낌. 톨게이트 느낌. 자동차가 지나가면 닫히고 다음 자동차는 닫힌 상태에서 기다려야 한다.
class Lock
{
// bool인데 커널에서 관리하는 변수라고 생각하면 된다.
AutoResetEvent _available = new AutoResetEvent(true);
public void Acquire()
{
_available.WaitOne(); // 입장 시도
// _available.Reset(); // bool = false;
// WaitOne 하면 자동으로 문을 닫아주기 때문에 Reset이 세트로 포함되어 있다.
}
public void Release()
{
_available.Set(); // flag = true
}
}
이렇게 하고 실행을 해보면
답이 안나오는 걸 볼 수 있다.
잘못한 건 없지만 다만 커널레벨로 가서 직원을 호출하는게 오래 걸려서 끝이 나지 않는 거 뿐이다.
internal class Program
{
static int _num = 0;
static Lock _lock = new Lock();
static void Thread_1()
{
for (int i = 0; i < 10000; i++)
{
_lock.Acquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 10000; i++)
{
_lock.Acquire();
_num--;
_lock.Release();
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(_num);
}
}
반복 횟수를 1000번 정도로 줄여보자.
0이 뜨는 걸 알 수 있다.
스핀락에 비해 확연히 느려진 것을 알 수 있다.
_num++; 같이 한줄 짜리가 아니라 오래 걸리는 코드의 경우는 이런 식으로 하는게 아주 나쁘다고 할 수는 없지만 일반적인 상황 MMORPG에서는 락을 잡고 오래동안 행동을 하는 거 자체가 문제가 있다.
이렇게 AutoResetEvent를 이용 방법에 대해 알아 봤다.
Event를 이용하는 방법은 Lock이 아니더라도 종종 나올 수 있다.
그 다음에 ManualResetEvent다.
class Lock
{
// bool인데 커널에서 관리하는 변수라고 생각하면 된다.
ManualResetEvent _available = new ManualResetEvent(true);
public void Acquire()
{
_available.WaitOne(); // 입장 시도
_available.Reset(); // 문을 닫는다.
}
public void Release()
{
_available.Set(); // 문을 열어준다.
}
}
이렇게 한번 실행을 해보자.
0이 아닌 수가 나온다.
뭔가가 문제가 있다.
입장을 시도하고, 문을 닫는게 나눠져 있기 때문에 문제가 되는 거다.
결국 Lock을 구현할 때 ManualResetEvent를 이용하는 건 잘못된 방법이 되는 거다.
AutoResetEvent를 호출하는게 훨씬 깔끔한 방법이 된다.
그럼 ManualResetEvent는 어느 때 사용해야 하는걸까?
경우에 따라 한번에 하나씩 입장시키는게 아닌 경우도 있다. 예를 들어
class Lock
{
// bool인데 커널에서 관리하는 변수라고 생각하면 된다.
ManualResetEvent _available = new ManualResetEvent(false);
public void Acquire()
{
_available.WaitOne(); // 입장 시도
}
public void Release()
{
_available.Set(); // 문을 열어준다.
}
}
_available을 false로 세팅한 다음에 어떤 작업이 끝났으면 로딩이든 패킷을 받는 작업이든 오래 걸리는 작업을 기다렸다가 그 작업이 끝났으면 재가동 할 수 있는 그런 코드를 만든다고 하면 ManualResetEvent를 사용하면, 대기하는 애들은 WaitOne만 실행하면서 대기하다가 최종적으로 허가를 내주는 애가 _available.Set();으로 문을 열어주면 나머지 애들은 신나게 들어오는 구현을 할 수 있을 거야. 다만 지금 구현하는 락이랑은 다른 시나리오가 될거야.
이렇게 Event를 사용하는 방법에 대해 알아봤어.
이런 AuotResetEvent든 ManualResetEvent든 커널단에 가서 요청을 한다는게 중요하고 차이에 대해 이해해야 한다.
스핀락은 뺑뺑이 돌면서 유저모드에서 실행하는 거고
Event는 아랫단의 운영체제에게 요청을 하는 것이기 때문에 한번만 해도 큰 부담이 된다.
이벤트 사용 말고도 커널을 이용해서 뭔가 순서를 맞추는 방법이 몇개가 더 있기는 하다. 그중에서 대표적으로 Mutex라는 애가 있다.
간단하게 개념만 보자.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
internal class Program
{
static int _num = 0;
static Mutex _lock = new Mutex();
static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
_lock.WaitOne();
_num++;
_lock.ReleaseMutex();
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
_lock.WaitOne();
_num--;
_lock.ReleaseMutex();
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(_num);
}
}
}
Lock을 지우고 Mutex를 넣어 이렇게 실행을 해보면 0이란 값이 나온다.
반복 횟수에 0을 추가해서 해보면 한참 뒤에 뜬다. 스핀락보다 느리다는 걸 알 수 있다.
Muitex 얘도 마찬가지로 커널동기화 객체라고 보면 된다.
식당에서 직원끼리 해결하는게 아니라 식당 관리자에게까지 가서 고자질을 한 상태라고 보면 된다. 걔가 직원 사이의 순서를 Mutex라는 애를 이용해서 알아서 맞춰주는 거라고 보면 된다.
AutoResetEvent랑 Mutex랑 뭐가 다를까?
하는 일은 비슷한데 Mutex는 조금 더 많은 정보를 갖고 있다.
Event가 bool로 이루어졌다고 하면 Mutex는 온갖 정보를 갖고 있어.
예를 들어 Mutex는 몇 번 잠궜는지 카운팅을 하고 있어.
static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
_lock.WaitOne();
Thread_2();
_lock.WaitOne();
_num++;
_lock.ReleaseMutex();
_lock.ReleaseMutex();
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
_lock.WaitOne();
_num--;
_lock.ReleaseMutex();
}
}
일렇게 두번 잠구는 경우가 있어.
두번 잠궜으면 두번 풀어야지만 최종적으로 락을 풀어주게끔 해준다.
그리고 int ThreadId라는 걸 가지고 있어서 나를 이렇게 Lock을 한 애가 누군지를 기억 했다가 Release를 했는데 엉뚱한 애가 나를 Release하면 문제가 있다는 거다. 그런 에러도 잡아주는 역할을 한다. 그러다 보니 추가 비용이 들어간다.
어지간해서는 AutoResetEvent만으로 충분하고 Mutex를 활용할 일은 거의 없다.
어쨌든 Evemt는 커널단에서 지원하는 애들이고 느리다는 걸 기억하자.
'Server programming' 카테고리의 다른 글
01_14_멀티쓰레드_ReaderWriteLock구현 연습 (0) | 2023.03.24 |
---|---|
01_13_멀티쓰레드_ReaderWriteLock (0) | 2023.03.21 |
01_11_멀티쓰레드_ContextSwitching (0) | 2023.03.15 |
01_10_멀티쓰레드_SpinLock (0) | 2023.03.15 |
01_09_멀티쓰레드_Lock 구현 이론 (0) | 2023.03.14 |
댓글