posted by 방랑군 2012. 1. 21. 22:44
멀티 스레드에서 가장 중요한 동기화 시키는 법에 대해 알아보겠습니다.

그중 MunualResetEvent 클래스를 사용해서 하는법을 알아보겠습니다.

ManualResetEvent는 서로 Signal(신호)를 통해 스레드를 작동시키는 것입니다.

그럼 필수적으로 필요한 매소드에 대해 알아보겠습니다.





ManualResetEvent의 Set() = 계속 진행되도록 신호를 받는것
ManualResetEvent의 Reset() = 스레드가 차단되어 이벤트 신호가 더이상없음을 설정하는
ManualResetEvent의 WaitOne() = 신호의 상태를 확인합니다. 
즉 Set()으로 되어있으면 true
Reset()으로 되어있으면 false 
반환합니다.
이라고 생각하시면 됩니다.

소스를 보면서 설명해 보겠습니다.기본 소스 입니다.
MyCalculator로 제가 만든 클래스 입니다.(클릭하면 크게 보입니다.)
ManualResetEvent클래스 변수 control_Thread 를 만든 후 초기화를 false로 했습니다.
WaitOne()함수는 control_Thread의 신호가 false인지,true인지 확인한 합니다.
그 후 for문을 빠져나오고 MultiNum()함수가 끝날 무렵,
반환될 무렵에 control_Thread.Reset()로 설정해 더이상
 보내지는 신호가 없게 인식하도록 하였습니다.
실행해 보면 결과값입니다.
밑에 그림에서 //MyCalculator.control_Thread.Set()을 주석을 사용하였더니 
커맨드 창에 MyCalculator의 MultiNum()함수가 작동을 하는것을 알수있습니다.
왜 작동을 안할까요?? 위 그림 설명을 하면서 마지막 함수가 반환될 때
 control.Thread.Reset() 으로 되어있기 때문입니다.
주석을 지워보았습니다. 
동기화가 모가 되는거냐 라고 하실것 같습니다. 그래서 밑에 더 소스를 추가 해 보겠습니다.
밑에 노란줄있는 부분이 바뀐 소스입니다.
중간에 Thread.Sleep(2000)을 하여 2초간 잠시 모든 스레드가 멈추도록 하였습니다.
그후 다시 MyCalculator.control_Thread.Set()을 호출하여 신호상태를 바꿔났습니다.
결과 물입니다.왼 쪽 결과물은 Thread.Sleep(2000)이 불리기 전이고 
오른쪽은 Thread.Sleep(2000)이 불린뒤 입니다.

저작자 표시 비영리

posted by 방랑군 2012. 1. 21. 22:44
윈도우 프로그래밍을 자주 하지 않다보니 스레딩의 개념을 이해하고 까먹고의 연속이다.
이참에 CodeProject의 내용을 참고하여 간단히 AutoResetEvent와 ManualResetEvent의 용법을 정리해 보려 한다.

개요
공용리소스(파일/변수 등)을 멀티스레딩 환경에서 동시에 접근할 때 항상 고민해야 하는 문제는 바로 Access 문제이다. 이러한 골치아픈 문제를 해결하기 위해 여러가지 기법들이 있는데, 여기선 AutoResetEvent와 ManualResetEvent의 용법을 알아보도록 하겠다.

비유
AutoResetEvent와 ManualResetEvent는 마치 철도 건널목의 차단기와 같다고 보면 된다. 
당신이 신호를 보내면 차단기가 올라가기도 하고(Set), 다른 신호를 보내면 차단기가 내려가기도 한다.(Reset)
자동차가 차단기 앞에 도착했을때(WaitOne) 차단기의 신호상태에 따라 통과 여부를 판단하게 된다.

 
통과 신호를 받을때까지 대기하고 있다.

통과 신호를 받으면 통과하게 된다.


구현예제

1초 마다 이벤트를 발생시키는 Timer 객체를 이용하여 샘플 코드를 구현해 봤다.

아래 코드는 앞의 timer_Elapsed 코드가 완료되었는지에 상관없이 무조건 1초 마다 이벤트를 발생시킨다.

timer_Elapsed 코드 안에서는 랜덤하게 Sleep을 걸어 불규칙적인 프로세스 실행 시간을 시뮬레이션 하였다.


  1. using System;  
  2. using System.Threading;  
  3.   
  4. namespace TestMultiThreadLock  
  5. {  
  6.     class Program  
  7.     {  
  8.         static System.Timers.Timer timer;  
  9.         static Random rnd = new Random(1000);  
  10.           
  11.         static void Main(string[] args)  
  12.         {  
  13.             //1초에 한번씩 이벤트 발생  
  14.             timer = new System.Timers.Timer();  
  15.             timer.Interval = 1000;  
  16.             timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);  
  17.             timer.Start();  
  18.   
  19.             Console.ReadKey();  
  20.         }  
  21.   
  22.         static void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)  
  23.         {  
  24.             DateTime curTime = DateTime.Now;  
  25.   
  26.             //임의의 프로세스 처리시간 대기  
  27.             int val = rnd.Next(0, 10000);  
  28.             Thread.Sleep(val);  
  29.   
  30.             Console.WriteLine(curTime.ToString("HH:mm:ss"));  
  31.         }  
  32.   
  33.     }  
  34. }  

실행결과는 아래와 같이 시간이 불규칙적으로 출력된다.


현재 코드는 단순 출력이지만 스레드에서 파일이나 for/foreach 문으로 종종 사용되는 Collection 객체에 억세스 했을 경우 심각한 예외상황이 발생할 가능성이 높아진다. (참고로 스레딩 안에서 발생되는 Exception은 바깥쪽 try/catch 구간에 잡히지 않은 채로 부모 프로세스마저 강제 종료되는 경우가 있으므로 스레딩 안쪽의 코드는 try/catch를 항상 구현하거나, 100% 신뢰 가능한 코드를 구현해야한다.)

따라서 위의 코드를 동기화, 즉 이벤트가 발생한 순서대로 실행하고, 후차로 진입한 함수는 앞선 함수가 끝날때 까지 대기시켜야 하는 방법을 ManualResetEvent로 해결해 보도록 한다.

  1. using System;  
  2. using System.Threading;  
  3.   
  4. namespace TestMultiThreadLock  
  5. {  
  6.     class Program  
  7.     {  
  8.         static ManualResetEvent mr = new ManualResetEvent(true);  
  9.         static System.Timers.Timer timer;  
  10.         static Random rnd = new Random(1000);  
  11.           
  12.         static void Main(string[] args)  
  13.         {  
  14.             //1초에 한번씩 이벤트 발생  
  15.             timer = new System.Timers.Timer();  
  16.             timer.Interval = 1000;  
  17.             timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);  
  18.             timer.Start();  
  19.   
  20.             Console.ReadKey();  
  21.         }  
  22.   
  23.         static void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)  
  24.         {  
  25.             mr.WaitOne();  
  26.             mr.Reset();  
  27.   
  28.             DateTime curTime = DateTime.Now;  
  29.   
  30.             //임의의 프로세스 처리시간 대기  
  31.             int val = rnd.Next(0, 10000);  
  32.             Thread.Sleep(val);  
  33.   
  34.             Console.WriteLine(curTime.ToString("HH:mm:ss"));  
  35.   
  36.             mr.Set();  
  37.         }  
  38.   
  39.     }  
  40. }  

출력결과는 아래와 같다.


static ManualResetEvent mr = new ManualResetEvent(true);  
구문에서 초기값은 차단기가 내려간 상태의 여부를 결정한다.
true이면 차단기가 올라간 상태에서 시작되며, 
false이면 차단기가 내려간 상태에서 시작된다.


즉 위의 샘플에서는 차단기가 올라간 상태에서 시작되었으므로, 처음 실행되는 mr.WaitOne(); 구문은 지체없이 통과하게 된다.
mr.WaitOne(); 바로 다음의 mr.Reset();은 본인이 건널목을 통과하자마자 차단기를 다시 내리는 것을 의미한다.
(마치 엘리베이터 먼저 들어가서 홀랑 문닫고 자기만 올라가는 이미지를 생각하면 되겠다.)


mr.Reset(); 구문 부터 ~ mr.Set(); 구문이 실행 될 때 까지, 이후에 실행되는  timer_Elapsed 이벤트는 모두 mr.WaitOne(); 구문에서 대기하게 된다.


이러한 원리로 스레드를 동기화 시켜 시간을 순서대로 출력 할 수 있다.


AutoResetEvent와의 차이점

여기서 AutoResetEvent 와 ManualResetEvent의 차이점을 비교하자면, 단순히 WaitOne의 대기상태가 Set으로 인해 풀린 이후, 곧바로 Reset을 수동으로 하느냐 자동으로 하느냐의 차이밖에 없다.
위의 샘플코드에서는 WaitOne이후에 곧바로 Reset을 수동으로 하므로, 사실은 AutoResetEvent로 구현하여 Reset();  부분을 삭제하면 완전히 동일하게 동작하게 된다.
따라서 Reset 부분을 WaitOne 이후 어떠한 동작 이후에 실행해야 한다면 ManualResetEvent 클래스를 사용하면 된다.



스레드 동기화 시에 주의할 점

위의 샘플코드는 사실 스레드 동기화를 위해서는 적합하지 않은 코드이다. 굳이 위의 샘플코드를 작성한 이유는 스레드 동기화시 발생할 수 있는 문제점을 지적하기 위해서다.  
동기화된 샘플코드의 timer 객체는 정확히 1초마다 이벤트를 발생하는데 반해, timer_Elapsed 이벤트는 불규칙한 시간을 대기하게 된다. 


만약 대기시간이 100초가 걸린다면, 스레드는 최대 100개가 생성되므로 퍼포먼스에 문제를 일으키게 된다.
(물론 Timer 클래스 옵션에 AutoReset을 이용하면 동기적으로 timer_Elapsed 이벤트 자체가 발생되지 않게 구현 가능하다.)
즉, 스레드 동기화시엔 과다한 스레드가 생성되지 않게 유의해야 한다. 퍼포먼스로 인한 오류는 파악 자체가 매우 어려우므로 처음 부터 제대로 설계하는 것이 후세에(?) 도움이 됨을 유의하도록 하자.



참고

더욱 많은 스레드 동기화 기법 및 자세한 설명은 다음 사이트를 참고하면 많은 도움이 될 것이다.