메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

C# 쓰레드 이야기: 11. 이벤트(Event)

한빛미디어

|

2002-04-25

|

by HANBIT

33,461

저자: 한동훈(traxacun@unitel.co.kr)

지난 시간에는 식사하는 철학자 문제와 뮤텍스에 대해서 소개했으며, 이번에는 뮤텍스의 나머지 부분에 대해서 알아보자. 그전에 이벤트에 대해서 알아보자.

이벤트 지금까지 살펴본 모니터나 뮤텍스는 하나의 쓰레드가 공유 데이터를 액세스하는데 유용하지만, 여러 개의 쓰레드가 서로의 실행을 방해하지 않으면서 쓰레드간에 메시지를 주고 받으려면 어떻게 해야할까?

이런 경우를 생각해보자. A 쓰레드가 작업이 끝나면 이 사실을 전달받은 B 쓰레드가 작업을 시작한다. 두 쓰레드간에 데이터를 공유할 필요도 없고, 동기화를 사용할 필요도 없다. 이와 같은 작업을 파이프 라인으로 물이 흘러가는 것과 같다고 해서 pipe-lined execution이라고도 한다. 이 경우에는 단지 대기중인 쓰레드에게 작업이 끝난다는 사실만을 전달해주면 된다. 이런 경우에 이벤트를 사용한다.
러닝 리눅스 3판

참고 도서

IT 백두대간, C# 프로그래밍
김대희


이벤트는 신호상태(Signaled)와 비신호상태(Non-Signaled) 두 가지 상태를 갖고 있다. 신호상태와 비신호상태라는 것은 마치 등이 하나뿐인 신호등과 같다. 신호등에 대기중인 쓰레드는 등이 켜지지 전(non-signaled)에는 대기하고, 신호등이 켜지면(signaled) 가던 길을 계속 갈 수 있게된다. 신호 메커니즘에 대한 보다 자세한 글은 지난 글을 참고하기 바란다.

이벤트는 두 가지 종류가 있다. 하나는 AutoReset 이벤트이며 다른 하나는 ManualReset 이벤트이다. 이들 각각의 이벤트는 실제로 닷넷에서 AutoResetEvent와 ManualResetEvent 클래스로 구현되어 있다. 이들 클래스의 생성자는 다음과 같다.
  public ManualResetEvent(bool initialState);
  public AutoResetEvent(bool initialState);
initialState는 true나 false를 사용할 수 있으며, 이벤트의 초기상태를 신호상태로 할 것인지, 비신호상태로 할 것인지를 지정한다. 대부분의 경우에 false를 사용한다.

먼저 AutoResetEvent를 사용하는 방법에 대해서 살펴보자. 이벤트를 생성하려면 다음과 같이 한다.

여기서는 각각의 작업 3개에 대해서 3개의 이벤트를 생성한다.
  public AutoResetEvent areStep1 = new AutoResetEvent(false);
  public AutoResetEvent areStep2 = new AutoResetEvent(false);
  public AutoResetEvent areStep3 = new AutoResetEvent(false);
위 예제에서는 false를 사용하기 때문에 비신호상태로 선언한다는 것을 알 수 있다. 비신호상태라는 것은 신호가 될 때까지 신호등 앞에서 기다린다는 의미로 해석하면 된다. 즉, 처음부터 이벤트는 대기상태가 된다. 필자는 쓰레드들의 각각의 작업이 Step1 → Step2 → Step3으로 수행되기를 원한다. 또한 이들 각각의 작업은 데이터를 공유하지 않으며, 단순히 대기중인 쓰레드에게 작업이 끝났다는 사실만을 알려주기 위해 이벤트를 사용한다. 다음은 Step1에 대한 함수이다.
  public void Step1()
  {
    Console.WriteLine("Processing Step1");
    Thread.Sleep(3000);
    areStep1.Set();
  }
먼저 화면에 Step1을 처리중이라는 메시지를 출력한다. 그리고 실제로 어떤 작업을 수행하는 함수가 들어가야하지만 여기서는 간단히 어떤 작업을 처리하는 것을 에뮬레이트하기 위해 Thread.Sleep(3000)을 사용하여 3초간 처리중인 것처럼 하였다. 처리가 끝나면 areStep1.Set()을 사용하여 첫번째 이벤트 areStep1을 신호상태로 변경한다. areStep1이 신호상태로 바뀐 것을 감지하고 작업을 수행하는 쓰레드는 Step2이다. 이제 Step2는 어떻게 Step1의 쓰레드가 이벤트를 신호상태로 바꾼 것을 알고, 작업을 처리하는지 살펴보자.
  public void Step2()
  {
    areStep1.WaitOne();
    Console.WriteLine("Processing Step2");
    Thread.Sleep(1000);
    areStep2.Set();
  }
가장 중요한 부분인데, areStep1.WaitOne()이라고 되어 있다. 즉, 첫번째 이벤트 areStep1이 신호상태가 될 때까지 기다린다는 것을 의미한다. 즉, 쓰레드가 처리하는 어떤 곳에서든지 areStep1이 신호상태가 되면 그것을 감지하고 대기중인 쓰레드를 잠에서 깨우는 역할을 하는 부분이다. Step2에서도 Thread.Sleep(1000) 대신에 DoPerformStep2()와 같이 어떤 함수를 사용할 것이지만 여기서는 작업을 에뮬레이트하기 위해 간단히 Thread.Sleep()을 사용하였다. 마찬가지로 작업이 끝난 다음에 이벤트 areStep2를 신호상태로 변경하여 다른 쓰레드들에게 알린다. Step3은 Step2와 동일하며, 단지 대기중인 이벤트만 다르다.

이에 대한 전체 소스는 다음과 같다.
이름 : event01.cs

using System;
using System.Threading;

public class AppMain
{
  public AutoResetEvent areStep1 = new AutoResetEvent(false);
  public AutoResetEvent areStep2 = new AutoResetEvent(false);
  public AutoResetEvent areStep3 = new AutoResetEvent(false);


  public void Step1()
  {
    // do something
    Console.WriteLine("Processing Step1");
    Thread.Sleep(3000);
    areStep1.Set();
  }

  public void Step2()
  {
    areStep1.WaitOne();
    Console.WriteLine("Processing Step2");
    Thread.Sleep(1000);
    areStep2.Set();
  }

  public void Step3()
  {
    areStep2.WaitOne();
    Console.WriteLine("Processing Step3");
    areStep3.Set();
  }


  public void DoTest()
  {
    Thread thread1 = new Thread(new ThreadStart(Step1) );
    Thread thread2 = new Thread(new ThreadStart(Step2) );
    Thread thread3 = new Thread(new ThreadStart(Step3) );

    Console.WriteLine("Thread 1, 2, 3 are started");
    thread1.Start();
    thread2.Start();
    thread3.Start();
}

  public static void Main()
  {
    AppMain ap = new AppMain();
    ap.DoTest();
  }
}
위에서는 AutoResetEvent를 살펴보았다. AutoResetEvent와 ManualResetEvent의 차이점은 여러분이 짐작하고 있는 것처럼 이벤트의 상태가 자동으로 초기화되느냐 그렇지 않느냐의 차이다. 위의 예제에서 Step2를 처리하는 쓰레드는 이벤트가 신호상태가 되기를 대기한다. 신호상태가 되면 이벤트는 쓰레드를 통과시키고 다시 자동으로 비신호상태가 된다. 따라서 직접 Reset()을 사용할 필요가 없다. 반면에 ManualResetEvent는 한 번 신호상태가 되면 다시 Reset()을 명시적으로 호출하여 비신호상태로 만들때까지 계속 신호상태를 유지한다.

참고로 Main()에서 두 개의 인스턴스를 생성하여 실행해보도록한다.
  public static void Main()
  {
    AppMain ap = new AppMain();
    ap.DoTest();
    AppMain ap2 = new AppMain();
    ap2.DoTest();
  }
이와 같이 변경한 다음에 실행해보면 두 인스턴스의 각각의 쓰레드들이 병렬적으로 실행된다는 것을 알 수 있을 것이다.(전부 몇 개의 쓰레드가 실행중인지 확인하고 싶다면 Thread.Sleep()의 시간을 충분히 늘려놓은 다음에 7회에서 작성한 WinTop을 이용해서 몇 개의 쓰레드가 실행중인지 확인해보는 것도 좋을 것이다) 또는 다음과 같이 Main()을 수정하고 실행해본다.
  public static void Main()
  {
    AppMain ap = new AppMain();
    ap.DoTest();
    ap.DoTest();
  }
즉, 특정 작업이 Step1 → Step2 → Step3으로 병렬적으로 실행되는데 좋다는 것을 알 수 있을 것이다. 대용량의 데이터 다섯 개를 동시에 1M씩 메모리로 읽어들이고, 1M씩 계산하고, 처리된 결과를 저장하는 것과 같은 단계별 작업이 필요하다면 최소한 3개의 쓰레드가 이벤트를 통해서 신호를 주고 받으면서 작업할 수 있을 것이다.(이 경우에 공유 데이터가 없다는 사실에 유의한다.)

수동 이벤트에 대해서 알아보기 위해 위 예제를 ManualResetEvent를 사용하도록 변경해보자.
  public ManualResetEvent mreStep1 = new ManualResetEvent(false);
  public ManualResetEvent mreStep2 = new ManualResetEvent(false);
  public ManualResetEvent mreStep3 = new ManualResetEvent(false);
ManualResetEvent 역시 AutoEventReset과 동일한 생성자를 갖는다. 여기서도 마찬가지로 세 개의 이벤트를 모두 비신호상태로 둔다. Step1은 다음과 같이 변경한다.
  public void Step1()
  {
    mreStep1.WaitOne();
    mreStep1.Reset();
    Console.WriteLine("Processing Step1");
    Thread.Sleep(3000);
    mreStep1.Set();
  }
WaitOne()으로 이벤트가 신호상태가 되기를 기다리는 코드를 추가하였다. AutoResetEvent와 달리 신호상태에서 하나의 대기 쓰레드를 통과시킨 다음에 자동으로 비신호상태가 되지 않으므로 Reset()을 호출하여 명시적으로 비신호상태로 전환한다.
이름: event02.cs

using System;
using System.Threading;

public class AppMain
{
  public ManualResetEvent mreStep1 = new ManualResetEvent(false);
  public ManualResetEvent mreStep2 = new ManualResetEvent(false);
  public ManualResetEvent mreStep3 = new ManualResetEvent(false);

  public void Step1()
  {
    mreStep1.WaitOne();
    mreStep1.Reset();
    Console.WriteLine("Processing Step1");
    Thread.Sleep(3000);
    mreStep1.Set();
  }

  public void Step2()
  {
    mreStep1.WaitOne();
    mreStep1.Reset();
    Console.WriteLine("Processing Step2");
    Thread.Sleep(1000);
    mreStep2.Set();
  }

  public void Step3()
  {
    mreStep2.WaitOne();
    mreStep2.Reset();
    Console.WriteLine("Processing Step3");
    mreStep3.Set();
  }


  public void DoTest()
  {
    Thread thread1 = new Thread(new ThreadStart(Step1) );
    Thread thread2 = new Thread(new ThreadStart(Step2) );
    Thread thread3 = new Thread(new ThreadStart(Step3) );

    thread1.Start();
    thread2.Start();
    thread3.Start();

    Console.WriteLine("Thread 1, 2, 3 are started");
  }

  public static void Main()
  {
    AppMain ap = new AppMain();
    ap.DoTest();
  }
}
위 코드를 컴파일하고 실행하면 아무것도 실행되지 않는다는 것을 알 수 있다. Step1에서도 mreStep1.WaitOne()으로 첫번째 이벤트 mreStep1이 신호되기를 기다리고 있기 때문이다. 따라서 DoTest를 다음과 같이 수정한다.
  public void DoTest()
  {
    Thread thread1 = new Thread(new ThreadStart(Step1) );
    Thread thread2 = new Thread(new ThreadStart(Step2) );
    Thread thread3 = new Thread(new ThreadStart(Step3) );

    thread1.Start();
    thread2.Start();
    thread3.Start();

    mreStep1.Set();

    Console.WriteLine("Thread 1, 2, 3 are started");
}
이벤트를 신호상태로 만들어준다. 컴파일하여 실행하면 AutoResetEvent를 사용한 것과 차이가 없을 것이다.(내부적으로는 어떻든간에 말이다)

여기서는 AutoResetEvent와 ManualResetEvent 모두 하나의 이벤트만을 기다리도록 하였다. 그러나 멀티 쓰레드 프로그램에서 어떤 종류의 작업은 동시에 일어나야하는 경우도 있다. AutoResetEvent는 여러 개의 쓰레드가 하나의 작업을 처리하기 위해서 메시지를 주고 받는데 유용하며, ManualResetEvent는 여러 개의 쓰레드가 동시에 여러 개의 작업을 처리하기 위해서 메시지를 주고 받는데 유용하다. 예를 들어서, 사용자가 워드 파일을 읽어들였을 때, 문서에 있는 단어수를 세는 쓰레드가 하나, 문서를 화면에 표시하는 쓰레드가 하나, 맞춤법을 검사하는 쓰레드가 하나, 인쇄를 하는 쓰레드가 하나. 이렇게 4개의 쓰레드가 문서를 읽어들이는 시점에 동시에 발생해야한다면 ManualResetEvent를 사용하도록 한다.

일반적으로 ManualResetEvent는 Set()을 호출하고 바로 Reset()을 호출하는 것이 대부분이기 때문에 Pulse()와 같은 메소드를 갖고 있어야하겠지만(Win32에서는 그렇다!) 닷넷에서는 이런 종류의 Pulse()는 없다. 따라서 필요하다면 자신이 만들어쓰도록 한다. 이러한 Pulse()는 Monitor 클래스에서 볼 수 있다.(Pulse나 PulseAll에 대해서는 MSDN을 참고한다)

마치며

뮤텍스와 이벤트가 무슨 관계가 있을까?라고 생각하는 분들도 있을 것이다. 이벤트가 뮤텍스의 소유권에 대한 상태를 알린다고 하면, 쓰레드들간에 이벤트를 주고 받음으로써 뮤텍스의 소유권을 넘겨 받을 수 있다는 것을 생각할 수 있을 것이다. 다음에는 이벤트와 뮤텍스를 이용하는 것에 대해서 살펴보도록 하자. 끝으로 다음에는 식사하는 철학자 문제를 이벤트(배고픔)와 뮤텍스(포크)를 사용해서 풀어보도록 하자.
TAG :
댓글 입력
자료실

최근 본 상품0