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 이벤트 자체가 발생되지 않게 구현 가능하다.)
즉, 스레드 동기화시엔 과다한 스레드가 생성되지 않게 유의해야 한다. 퍼포먼스로 인한 오류는 파악 자체가 매우 어려우므로 처음 부터 제대로 설계하는 것이 후세에(?) 도움이 됨을 유의하도록 하자.



참고

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