오늘은 지난 시간에 이어서
마지막 방법으로 Lock을 구현을 해볼 것인데, (마지막 방법이 직원에게 알려달라고 한다음 나는 자리로 가있고 직원이 알려주면 그제서야 가는것)
이게 사실은 Event 라는 것을 사용하는 방법이다.
지난번에 얘기했던 마지막 방법이 이것이다.
이 방법은 직원이라는 제 3자를 개입시켜서 부탁을 해가지고
자물쇠가 열리면 알려달라고 역으로 부탁하는 방법이다.
그런데 이제 솔직하게 고백을 할 때가 왔는데
이 제3자(자물쇠가 비었으면 알려달라고 한 직원)은 사실 식당 직원이 아니다!
이 직원은 사실 Kernel Level 즉, 관리자 쪽에 있는 직원이다.
그래서 아까보다 사실은 더 느리다 라고 생각하면 된다.
이런식으로 Kernel Level로 왔다 갔다 하는것은
Context Switching이라는 것이 개입이 되니까( => 부담이 많이 되고 느리다?!)
어마어마하게 느리다라는 "단점"이 있을 것이다.
그대신 본인 입장에서 보면은
전혀 시간을 낭비하지 않고
무조건 100%확률로 자물쇠가 풀릴 때만 행동을 시작하는 "장점"이 있기는 하다.
그래서 이것을 코드로 구현을 해볼 것인데
C#에서 Event를 사용을 할 때는 사실 두가지 방법이 있다.
첫번째 방법으로는
"AutoResetEvent" 라는 것이 있는데
이것은 톨게이트를 생각을 하면된다.
톨게이트를 보면 차가 한대씩 한대씩 지나 갈 것이다.
차가 지나가면은 자동으로 톨게이트가 닫혀가지고
다음차는 대기를 해야 되는 그런 상황이 발생을 할 것인데
이게 바로 조금있다가 사용할 AutoResetEvent의 개념이라고 보면 된다.
그리고 "AutoResetEvent" 깐부같은 느낌으로
깐부도 알아야 하니까
"Manual Reset Event" 라는 것도 알아 볼것인데
이녀석은 그냥 방문 같은 느낌이다.
그래서 문이 수동으로 잠구어 지기 때문에
문을 열고 지나간다음에 문을 잠구지 않으면
몇대의 차든(몇명의 직원이든, 몇개의 쓰레드든) 막 신나게 지나갈 것이다.
그래서 코드로 가보자!
이제
이런식으로 뺑뺑이를 도는 것이 아니라(지워주도록 하자)
이런식으로 싸그리 다 날려도 되고
이제는 아까말한 Kernel 간에 요청을 하는 것이기 때문에
여기 위에다가 이제는 새로운 클래스를 하나 만들어야 한다.
AutoResetEvent라고 바로 뜨는데
이름은 이렇게 만들어 주면, 이제 인자로 initialState라는 것을 넣어주는 것을 볼 수 있는데
즉, true나 false둘중 하나로 넣어주는 것인데
만약 true라면 말 그대로 available한 상태 즉, 아무나 들어 올 수 있는 상태를 말하는 것이고
false인 경우에는 avialble하지 않은 상태이니까
즉, 문이 닫힌 상태 == 아무도 들어 올 수 없는 상태가 되는 것이다.
그래서 처음에
AutoResetEvent _available = new AutoResetEvent(true);
이렇게 톨게이트를
연 상태로 시작을 할 것인지
닫힌 상태로 시작을 할 것인지를 정해주고 시작을 할 수 있다.
그리고 이제 이녀석을 사용을 해볼 것인데
사용을 하는것은 사실 굉장히 쉽다.
_available.WaitOne(); // 입장 시도
라는 녀석은 입장을 시도하는 녀석이다.
그래서 만약에 이 _available 라는 값이
(내부적으로는 Kernel입장에서는 Boolean 값이라고 생각을 하면된다.)
그래서 이 녀석은 아랫단계에서 Kernel이 관리를 하는 그런 변수라고 생각을 하면 된다.
이제 보면은
현재 avilable은 true이니까
_available.WaitOne(); // 입장 시도
이런식으로 입장을 시도를 하면
무조건 입장이 된다.
그리고 이 WaitOne의 특징
더 정확히는 AutoResetEvent의 특징은 뭐냐 하면은
여기 앞에 Auto가 붙는 것은 뭔가 자동으로 해준다는 의미가 되는데
"뭐"를 자동으로 해주냐 하면은
_available.WaitOne(); // 입장 시도
입장을하고
"문"을 닫는 것을 자동으로 해준다는 의미가 된다!
그래서 정확히 톨게이트 같은 느낌이다.
그리고
이부분에서는
_available.WaitOne(); // 입장 시도
이거 한줄만 달랑 넣어주면 된다.
그리고 이제
Release를 할때, 즉 풀어주고 싶을 때는 이제
Set이라는 녀석이 있는데 이것을 읽어보면은
(이벤트 신호를 보낸다, 하나 또는 여러개의 기다리는 쓰레드들에게 라는 말같다 즉, 자물쇠가 풀렸다고 알려주겠다! 라는 말인듯?)
위에 ( ) 내가 해석한 것이 아니라
event의 상태를 sugnaled 상태로 바꾼다 == Boolean값인 _available을 다시 true로 켜준다는 말이다.
그리고 사실은
_available.Reset(); // bool -> false
Reset은 Boolean을 false로 넣어주는 개념이기는 한데
이게(Reset)이 사실은 WaitOne에 세트로 포함이 되어있다고 생각을 하면된다.
WaitOne으로 입장을 했으면 이제 닫기 위해 false로 바꿔주기 떄문에
_available.Reset이 WaitOne에 생략이 되어있다고 생각을 하면된다.
Set같은 경우에는 flag - true로 바꾸어준다. 즉 boolean값을 다시 true로 해준다는 말이다.
그래서 이제 코드를 실행을 해보면
나는 지금
답이 잘 나오는데..
강의에서는 지금 멈춘 상태로 나오고 있다.
그래서 아무튼 우리가 뭘 잘못을 했냐 해서 살펴보면은
딱히 잘못을 한것은 없다.
다만, 이제는 직원을 불러가지고 그러니까 Kernel Level에 가서 요청을 하기 때문에
어마어마하게 오래 걸려서 끝이 나지 않는 것이다.
for문을 백만번쯤으로 하니까 멈추어 있다가
이렇게 좀 기다리면 값이 나온다...! 성능 차이인듯!
그런데 지금 이렇게
한줄 짜리가 아니라
진짜로 오래 걸릴 수도 있는 거였으면 사실
이런식으로 하는것도 아주 나쁘다고는 할 수는 없지만
특히 MMO같은 경우에서는
여기 이렇게 락을 잡은다음에
뭔가 행동을 오랫동안 한다는 거 자체가 "문제"가 있다는 얘기이다.
어쨋든 이렇게 해서 AutoResetEvent에 대해서 알아보았다!
참고로 이렇게 Event를 이용하는 방법은 Lock이 아니다 하더라도
종종 나올 수는 있다.
그러면 이제 이어서
Manual Reset Event 에 대해서 얘기를 하고 넘어가도록 하자.
그래서 이런식으로 바꿔치기 하면은
이제
그런데 이녀석은 이제 톨게이트 느낌이 아니라 방문같은 개념이라고 했었었다.
우리가 그래서
WaitOne을 해가지고
기다리고 있으면은 이제
Kernel딴에서 즉, 아래있는 관리자 직원이
만약에
여기 있는 애가
flag가 지금 true인 상태여가지고
즉, 문이 열려있다면 통과를 시켜주기는 할 것이다.
"문제"는 뭐냐 하면은 아까와는 달리
자동으로 닫히지는 않는다고 했었었다.
그래서
_available.Reset();
이부분이 사실은 빠진 것이다.
그렇다면 우리가 이렇게 Reset();으로 닫아주면 되는건가 싶기는 하다..
그래서 이런 느낌으로 되는데
그래서 이렇게하고 이게 해결책이 되는 것인지 실행을 함 해보도록 하자.
실행을 해보면
아까는 0이였는데
다른 이상한 값이 나오기 시작했다.
(경합)
그래서 뭐가 "문제"냐고 하면은
사실 이부분을 굉장히 반복적으로 하고있는데
지금 여기를 보면은
_available.WaitOne(); // 입장 시도
이렇게 입장을 하고
_available.Reset(); // 문을 닫는다.
문을 닫는 작업이 이렇게 두단계로 나뉘어져 있기 때문에
"문제"가 되는 것이다.
그래서 결국
이 Lock을 구현을 한다고 했을 때
ManualResetEvent를 활용을 한다는 것은
조금 잘못된 방법이 될 것이다.
그래서 아까 AutoResetEvent를 호출하는 것이 훨씬더 깔끔한 방법이 될 것이다.
애당초 이녀석을 묶어서 한번에 실행을 했어야 우리가 원하는 결과를 얻을 수 있었을 것이다.
그러면 반대로 생각을 해서
ManualResetEvent는 언제 사용을 해야되는 것인지
긴가민가 하다.
그런데 경우에 따라서
_available.WaitOne(); // 입장 시도
이런식으로 한번에 하나씩만 입장을 시켜야되지 않을 경우도 생길 수 있다.
예를들어
_available를 다 false로 맞춘다음에
어떤 작업이 끝났다고 가정을하면 (로딩이든 패킷을 보내는 작업이든 뭐든)
뭔가 어마어마하게 오래 걸리는 작업을 기다렸다가,
작업이 끝났으면은
모든 쓰레드들이 이제 신나게 재가동을 하는 그런 코드를 만든다고 가정을 하면은
이런식으로
ManualResetEvent를 사용을 하면은
대기 하는 녀석들은
_available.WaitOne(); // 입장 시도
만 실행을 하면서, 기다리고 있다가
최종적으로 "허가"를 내어주는 애가
여기서 이렇게 문을 열어주면은
나머지 애들은
이렇게 신나게 들어오는 그런 구현을 할 수 있을 것이다.
다만 우리가 구현하고 있는 Lock과는 다른 시나리오가 될 것이다.
그래서
AutoRestEvent든 ManualResetEvent든
둘다 Kernel안으로 가가지고 요청을 한다는 사실이 굉장히 중요하고
이 "차이"에 대해서 굉장히 잘 이해를 해야한다.
우리가 이전에 얘기한
SpinLock같은 경우는 while문에서 계속 뺑뺑이를 돌면서
UserMode에서 계속 실행을 하는 것이였고
ManualResetEvent, AutoResetEvent들은 아랫단에서 Kernel에게 요청을 하기 떄문에
한번만 하더라고 굉장히 큰 부담이 된다고 했었었다.
그런데 사실은
이런 Event를 사용하지않고
Kernel을 사용해서 순서를 맞추는 방법이 몇가지 더 있기는 하다.
그 중에서도 가장 대표적으로 Mutex 라는 애가 있는데
그래서 이부분
지우고,
이렇게 수정읗 해주자.
그리고
이렇게 .을 찍어보면은
이녀석도 WaitOne이라는 게 있는 것을 볼 수 있다.
Event와 비슷 한 역할을 하는 것인데
이것도 같이 해보면은
이녀석도 똑같이 입장을 시도를 하려는 것이고
그래서 다 사용을 했다면
_lock.ReleaseMutex(); // 다사용한 후
라는 애가 있고
이렇게 수정을 해주자.
그리고 이것을 실행을 해보면
0이라는 값이 잘 나온다!
for문을 많이 돌면 속도가 느린것을 볼 수 있는데
도대체 이녀석은 왜 느릴까를 곰곰히 생각을 해보면
이녀석도 마찬가지로
Kernel까지 가는 Kernel동기화 객체라고 보면 된다.
그래서 우리가 식당에서 일어나는 일들을 식당직원들끼리만 해결을 할 수 있으면 좋은데
여기 아랫단(Kernel Mode)에다가 까지 가가지고 고자질을 한 상태라고 보면 되고
그러면
이제 아까 얘기한 AutoResetEvent랑 Mutex랑 뭐가 다른지 궁금할 수 있다.
사실은 하는 일은 비슷한데
Mutex 가 조금더 많은 정보를 가지고 있다.
AutoResetEvent가 Boolean값 하만 가지고 있다고 하면은
Mutex는 온갖 정보를 다 가지고 있는데
Mutex는 이녀석을 몇번이나 잠구었는지 카운팅을 하고 있다.
그래서 혹시라도 경우에 따라서
이렇게 두번 잠구고 싶은 경우도 생길 수 있는데
여기서 잠구고 어떤 함수를 호출하면서 동작하는 것도 있을 것이고
어쩃든
잠구는 만큼 풀어줘야 동작을 할 수 있게 할 수도 있고
그리고
이렇게 Thread_ 의 Id도 가지고 있어서
락을 한 애가 누군지를 기억을 하고 있다가
만약 이상한 엉뚱한 애가 Release를 하려고 했다는 것은
문제가 있다는 말이다.
그래서 그런 에러들도 잡아주는 여러가지 역할을 하는데
그렇다 보니까 사실은 추가 비용이 굉장히 많이 들어간다.
우리같은 경우에는 사실은 어지간해서는
AutoResetEvent만으로도 충분하고
static Mutex _lock = new Mutex();
이런식으로 Mutex를 활용할 일은 거의 없다고 보면 된다.
그래서 이런게 있다고 정도만 알고있으면되고
AutoResetEvent
ManualResetEvent
Mutex
세가지는 Kernel딴에서 거쳐서 동작을 한다고 까지만 알면된다!