programming/.net2010.01.10 00:03
윈도우 프로그래밍을 자주 하지 않다보니 스레딩의 개념을 이해하고 까먹고의 연속이다.
이참에 CodeProject의 내용을 참고하여 간단히 AutoResetEvent와 ManualResetEvent의 용법을 정리해 보려 한다.

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

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

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

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

구현예제

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

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

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


using System;  
using System.Threading;  
  
namespace TestMultiThreadLock  
{  
    class Program  
    {  
        static System.Timers.Timer timer;  
        static Random rnd = new Random(1000);  
          
        static void Main(string[] args)  
        {  
            //1초에 한번씩 이벤트 발생  
            timer = new System.Timers.Timer();  
            timer.Interval = 1000;  
            timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);  
            timer.Start();  
  
            Console.ReadKey();  
        }  
  
        static void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)  
        {  
            DateTime curTime = DateTime.Now;  
  
            //임의의 프로세스 처리시간 대기  
            int val = rnd.Next(0, 10000);  
            Thread.Sleep(val);  
  
            Console.WriteLine(curTime.ToString("HH:mm:ss"));  
        }  
  
    }  
}  
실행결과는 아래와 같이 시간이 불규칙적으로 출력된다.


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

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

using System;  
using System.Threading;  
  
namespace TestMultiThreadLock  
{  
    class Program  
    {  
        static ManualResetEvent mr = new ManualResetEvent(true);  
        static System.Timers.Timer timer;  
        static Random rnd = new Random(1000);  
          
        static void Main(string[] args)  
        {  
            //1초에 한번씩 이벤트 발생  
            timer = new System.Timers.Timer();  
            timer.Interval = 1000;  
            timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);  
            timer.Start();  
  
            Console.ReadKey();  
        }  
  
        static void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)  
        {  
            mr.WaitOne();  
            mr.Reset();  
  
            DateTime curTime = DateTime.Now;  
  
            //임의의 프로세스 처리시간 대기  
            int val = rnd.Next(0, 10000);  
            Thread.Sleep(val);  
  
            Console.WriteLine(curTime.ToString("HH:mm:ss"));  
  
            mr.Set();  
        }  
  
    }  
}
출력결과는 아래와 같다.


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



참고
더욱 많은 스레드 동기화 기법 및 자세한 설명은 다음 사이트를 참고하면 많은 도움이 될 것이다.
저작자 표시 동일 조건 변경 허락
신고
Posted by 귀뫄뉘

댓글을 달아 주세요

  1. 집시

    도움말을 보고서도 이해가 가지 않아서 찾았습니다.
    많은 도움 되었습니다.
    AutoResetEvent 사용 관련하여 좀더 찾아 가야 겠습니다.

    2011.06.29 15:38 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. ani

    굿 정말 깔끔하고 이해하기 쉽네요
    많은 도움 되었습니다 꾸벅 ( ..)

    2012.01.04 13:23 신고 [ ADDR : EDIT/ DEL : REPLY ]
  3. 초보

    thread 를 사용해서 데이타베이스를 계속 감시를 합니다
    이때 데이타베이스에 insert 가 되면 윈폼에서 처리를 하려고 합니다
    어떻게 해야하는지 감이 안오네여

    2012.11.07 17:22 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 이제 봤네요. ^^; 데이터 베이스 insert시 뭔가 발생하는 거라면 DB에서 trigger를 쓰는게 더욱 편하고 정확할 것이라 생각됩니다. trigger를 확인해보세요~

      2012.12.23 11:09 신고 [ ADDR : EDIT/ DEL ]
  4. claz

    참 궁금했었는데.. 그냥.. 모르고 넘기다.. 이제야.. 이글을 보고.. 사용처를 알았네요.. 감사합니다.

    2013.04.10 08:54 신고 [ ADDR : EDIT/ DEL : REPLY ]
  5. 쓰레드

    Thread 이용해서 큐에 데이터가 있으면 데이터를 DB에 입력 하려하는데
    혹시 힌트라도 얻을 수 있을까요??

    2013.06.17 22:01 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 음.. 방향을 잘 정하신거같은데 어떤 힌트가 필요하신지 모르겠네요~ 그냥 해보시면 될것같아요 ^^

      2013.06.18 10:44 신고 [ ADDR : EDIT/ DEL ]
  6. kjs077

    감사합니다. 퍼갈께요 ~!

    2013.07.05 11:23 신고 [ ADDR : EDIT/ DEL : REPLY ]
  7. 정헌

    잘 정리해 두셨네요~ 잘 보고 갑니다 ^^

    2013.11.20 18:02 신고 [ ADDR : EDIT/ DEL : REPLY ]
  8. GOD OF GOD

    진짜 깔끔한 정리 감사드립니다

    2016.06.30 13:27 신고 [ ADDR : EDIT/ DEL : REPLY ]
  9. GOD OF GOD

    안녕하세요 테스트 해보았는데
    Menual로 하면 동기화가 안되네요..
    auto로 하고 reset을 지우면 동기화가 적용되네요..

    2016.06.30 14:06 신고 [ ADDR : EDIT/ DEL : REPLY ]