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

한빛출판네트워크

IT/모바일

C# 쓰레드 이야기: 6. 쓰레드 예외 처리

한빛미디어

|

2001-11-30

|

by HANBIT

35,044

쓰레드의 예외 처리는 여러 가지가 있지만, 여기서는 몇 가지만을 간단히 다룰 것이다. 그러니까 늘 그렇듯이 이 글은 아주 짧다. ^^ 이전하고 비슷하게 이번에는 4개의 메시지를 출력하는 각각의 쓰레드를 만들어보자. 예제는 다음과 같다.
이름: ThreadEx.cs

using System;
using System.Threading;
using System.ComponentModel;

class ThreadExceptionApp
{
  public static void Main()
  {
    ThreadExceptionApp te = new ThreadExceptionApp();
    te.DoTest();
  }

  private void DoTest()
  {
    try
    {
      Thread[] myThreads = 
      {
        new Thread( new ThreadStart(SayHello) ),
        new Thread( new ThreadStart(SayMerry) ),
        new Thread( new ThreadStart(SayPapi)  ),
        new Thread( new ThreadStart(SayHappy) )
      };

      foreach(Thread myThread in myThreads)
      {
        myThread.Start();
      }

    }

    catch(ThreadStateException)
    {
      Console.WriteLine("--- ThreadStateException occured.");
    }
  } // end of function DoTest()

  private void SayHello()
  {
    for(int LoopCtr = 1; LoopCtr < 10; LoopCtr++)
    {
      Console.WriteLine("Hello, everyone.");
      Thread.Sleep(100);
    }
  }

  
  private void SayMerry()
  {
    for(int LoopCtr = 1; LoopCtr < 10; LoopCtr++)
    {
      Console.WriteLine("Merry~~~~~~");
      Thread.Sleep(100);
    }
  }

  private void SayPapi()
  {
    for(int LoopCtr = 1; LoopCtr < 10; LoopCtr++)
    {
      Console.WriteLine("Papi sounds good.");
      Thread.Sleep(100);
    }
  }

  private void SayHappy()
  {
    for(int LoopCtr = 1; LoopCtr < 10; LoopCtr++)
    {
      Console.WriteLine("All, Happy Programming.");
      Thread.Sleep(100);
    }
  }
} // end of class ThreadExceptionApp
예제를 실행하면 각각의 메시지가 잘 출력되는 것을 확인할 수 있다. 전에 설명한 것과 같이 쓰레드를 인터럽트하는 Interrupt(), 다른 쓰레드의 실행을 기다리는 Join(), 쓰레드를 중지시키는 Abort()가 있다. 그리고 이들 메소드중에 Interrupt()와 Abort()는 예외를 발생시키며, 각각의 예외는 ThreadInterruptedException과 ThreadAbortException이다. 먼저 ThreadInterruptedException에 대해 알아보기 위해 예제에서 먼저 DoTest() 함수에서 다음 부분을 변경하도록 한다.
foreach(Thread myThread in myThreads)
      {
        myThread.Start();
      }

      Thread.Sleep(5);

      myThreads[1].Interrupt();
myThread[1]은 SayMerry 함수를 뜻한다. Thread.Sleep(5)와 같이 약간의 지연 시간을 준 것은 바로 이전 코드가 쓰레드를 시작하는 부분이기 때문에 실제 코드가 시작하기 전에 쓰레드가 인터럽트 되는 것을 막기 위한 것이다. 즉, 쓰레드가 실제로 시작되도록 하기 위해 약간의 지연 시간을 주었다.
private void SayMerry()
  {
    try
    {
      for(int LoopCtr = 1; LoopCtr < 10; LoopCtr++)
      {
        Console.WriteLine("Merry~~~~~~");
        Thread.Sleep(100);
      }
    }

    catch(ThreadInterruptedException)
    {
      Console.WriteLine("--- ThreadInterrupedException - SayMerry() ---");
    }
}
쓰레드가 인터럽트되는 경우에 실제 쓰레드로 실행되는 함수에서 예외를 잡아야 한다. 이제 코드를 다시 컴파일하고 실행하면 결과는 다음과 같을 것이다.
Hello, everyone.
Merry~~~~~~
Papi sounds good.
All, Happy Programming.
All Thread ended.
--- ThreadInterrupedException - SayMerry() ---
Hello, everyone.
Papi sounds good.
All, Happy Programming.
볼 수 있는 것처럼 중간에 쓰레드가 인터럽트되었다는 것을 알 수 있다. 다른 예외로는 이미 죽어있는 쓰레드에 어떤 실행을 기대하는 경우가 있을 수 있다. 이러한 경우에는 ThreadStateException 예외가 발생한다. 예를 들어 다음 코드와 같이 한 번 죽인 쓰레드를 다시 죽이면 예외가 발생한다.
  MyThreads[1].Abort();    // 쓰레드를 죽인다.
  … some code …
  myThreads[1].Resume();   // 쓰레드의 실행을 계속하라고 지시한다.
이 경우에 쓰레드를 이미 종료했으므로 원하는 대로 쓰레드가 계속 실행되지 못할 것이다. 예제에서 DoTest() 함수를 다음과 같이 변경해보도록 하자.
private void DoTest()
{
    try
    {
      Thread[] myThreads = 
      {
        new Thread( new ThreadStart(SayHello) ),
        new Thread( new ThreadStart(SayMerry) ),
        new Thread( new ThreadStart(SayPapi)  ),
        new Thread( new ThreadStart(SayHappy) )
      };

      foreach(Thread myThread in myThreads)
      {
        myThread.Start();
      }

      Thread.Sleep(5);

      myThreads[1].Abort();    // 쓰레드를 중지시킴

      for(int LoopCtr = 1; LoopCtr < 100; LoopCtr++)
      {
        //do nothing
      }
       
      myThreads[1].Resume();   // ThreadStateException 예외 발생
    }

    catch(ThreadStateException)
    {
      Console.WriteLine("--- ThreadStateException occured.");
    }
  } // end of function DoTest()
Thread.Sleep(5)은 각각의 쓰레드가 실제로 시작하도록 하기 위한 약간의 지연 시간이다. 이와 같은 지연 시간이 없다면 Start() 다음에 Abort()를 호출하기 때문에 쓰레드의 상태는 Unstarted가 된다.
  myThreads[1].Abort();
SayMerry 함수를 위임한 두 번째 쓰레드를 중지시킨다. 1부터 100까지 루프를 실행하는 부분은 실제로 수행되는 코드를 대신하기 위한 것이다. 이제 중지된 쓰레드를 계속 실행하도록 Resume()을 호출하면 예외가 발생한다. 코드를 다시 컴파일하고 실행하면 결과는 다음과 같을 것이다.
Hello, everyone.
Merry~~~~~~
Papi sounds good.
All, Happy Programming.
--- ThreadStateException occured.
Hello, everyone.
Papi sounds good.
All, Happy Programming.
쓰레드의 상태를 알려면 ThreadState 속성을 사용하면 된다.
Console.WriteLine("Thread State : " + myThreads[3].ThreadState);
일반적으로 쓰레드의 예외처리는 SayMerry와 같이 쓰레드에 위임되는 함수에서 처리하며, 쓰레드 상태와 같은 몇 가지 특별한 예외들은 쓰레드를 이용하는 DoTest()와 같은 함수에서 처리한다는 것에 주의하기 바란다. 다음으로 알아볼 것은 Abort()를 사용할 때 발생하는 ThreadAbortException 예외다. 이 예외는 잡을 수 없는, 즉 catch 할 수 없는 특별한 예외이다. 예외가 발생하면 쓰레드를 강제로 죽이기 전에 쓰레드에 위임된 함수의 finally 블록을 실행한다. 따라서 finally 블록에서 예기치 않은 연산을 수행할 수도 있기 때문에 쓰레드의 종료를 보장하기 위해 Join()등을 호출해야 한다. Join()은 쓰레드가 실행을 멈출때까지 반환하지 않기 때문에 쓰레드를 블로킹하는 호출이다. 다시 말해서, Abort()를 사용했을 때 쓰레드의 상태가 WaitSleepJoin이 아니면 예외가 발생하지 않는다. 이것은 실제로 Abort()를 사용할 때 미묘한 문제를 일으키게 된다. 지금부터 예제를 살펴보면서 알아보도록 하자. 먼저 앞에서 사용한 예제에서 쓰레드를 WaitSleepJoin 상태로 만들 수 있는 모든 코드를 제거할 것이다. 먼저 각각의 Say 함수의 Thread.Sleep()을 제거하고, 각각의 Say 함수의 루프는 100회씩 반복하도록 수정한다. 마찬가지로 DoTest() 함수도 수정한다. 수정된 DoTest(), Say 함수들은 다음과 같다.
private void DoTest()
  {
    try
    {
      Thread[] myThreads = 
      {
        new Thread( new ThreadStart(SayHello) ),
        new Thread( new ThreadStart(SayMerry) ),
        new Thread( new ThreadStart(SayPapi)  ),
        new Thread( new ThreadStart(SayHappy) )
      };

      foreach(Thread myThread in myThreads)
      {
        myThread.Start();
      }

      for(int LoopCtr = 1; LoopCtr < 1000; LoopCtr++)
      {
        //do nothing
      }

      Console.WriteLine(" --- Thread State : " + myThreads[1].ThreadState);
      myThreads[1].Abort();
      Console.WriteLine(" --- Thread State : " + myThreads[1].ThreadState);
    }

    catch(ThreadStateException)
    {
      Console.WriteLine("ThreadStateException occured");
    }
  } // end of function DoTest()

  private void SayHello()
  {
    for(int LoopCtr = 1; LoopCtr < 100; LoopCtr++)
    {
      Console.WriteLine("Hello, everyone.");
    }
  }

  
  private void SayMerry()
  {
    try
    {
      for(int LoopCtr = 1; LoopCtr < 100; LoopCtr++)
      {
        Console.WriteLine("Merry~~~~~~");
      }
    }

    catch(ThreadAbortException)
    {
      Console.WriteLine("------------------------");
      Console.WriteLine("--- SayMerry Aborted ---");
      Console.WriteLine("------------------------");
    }

    finally
    {
      Console.WriteLine("------------------------");
      Console.WriteLine("--- SayMerry finally ---");
      Console.WriteLine("------------------------");
    }
  }

  private void SayPapi()
  {
    for(int LoopCtr = 1; LoopCtr < 100; LoopCtr++)
    {
      Console.WriteLine("Papi sounds good.");
    }
  }

  private void SayHappy()
  {
    for(int LoopCtr = 1; LoopCtr < 100; LoopCtr++)
    {
      Console.WriteLine("All, Happy Programming.");
    }
  }
먼저 DoTest() 함수에서 Abort()를 사용하기 전에 쓰레드의 상태를 출력하도록 한다. 이 경우에는 사용자마다 조금씩 다른 결과를 볼 수 있다. 어떤 사용자는 Running과 Aborted 상태만을 볼 수 있는가 하면 어떤 사용자는 AbortRequested와 Stopped를 보게 될 수도 있다.
Console.WriteLine(" --- Thread State : " + myThreads[1].ThreadState);
      myThreads[1].Abort();
      Console.WriteLine(" --- Thread State : " + myThreads[1].ThreadState);
SayMerry()에서는 예외가 발생한 부분을 알아보기 쉽도록 하기 위해서 위와 같이 하였다. 여기서는 쓰레드를 WaitSleepJoin으로 하는 부분을 배제했기 때문에 catch 블록은 실행되지 않고 finally 블록만 실행된다는 것에 주의하기 바란다. 코드를 실행하고 컴파일하면 다음과 같은 결과를 볼 수 있을 것이다. 다음 결과는 편의상 생략한 부분이 있다.
[생략]
Merry~~~~~~
Merry~~~~~~
------------------------
--- SayMerry finally ---
------------------------
Papi sounds good.
Papi sounds good.
[생략]
All, Happy Programming.
All, Happy Programming.
All, Happy Programming.
[생략]
 --- Thread State : Running
 --- Thread State : Aborted
결과에서 알 수 있는 것처럼 Abort()를 호출했지만, 예외가 발생하지 않았다. 이제 DoTest() 함수를 다음과 같이 변경해 보도록 하자.
for(int LoopCtr = 1; LoopCtr < 1000; LoopCtr++)
      {
        //do nothing
      }

      Thread.Sleep(10);
      myThreads[1].Abort();
예제에서 볼 수 있는 것처럼 Abort()를 호출하기 전에 Thread.Sleep()과 같이 블로킹하는 부분이 있으면 ThreadAbortException을 잡을 수 있다는 것을 알 수 있다. 또한 Thread.Sleep()이 없으면 시작과 함께 종료되기 때문에 실제 쓰레드 상태가 Unstarted로 프로그램이 종료되게 된다. 따라서 실제 쓰레드를 시작한 다음에 종료하기 위해 Thread.Sleep()을 넣었다. 결과는 다음과 같을 것이다.
Papi sounds good.
Papi sounds good.
Papi sounds good.
------------------------
--- SayMerry Aborted ---
------------------------
------------------------
--- SayMerry finally ---
------------------------
Thread.Sleep으로 인해 쓰레드의 상태가 WaitSleepJoin이 되기 때문에 예외가 발생한다. 마찬가지로 Join이나 Suspend 등으로 쓰레드가 블로킹되어 있는 경우에 Abort()를 사용할 경우에는 예외를 잡을 수 있으나 그렇지 않은 경우에는 예외를 잡을 수 없다. 따라서 Abort()로 쓰레드를 죽인 다음에, 쓰레드에 위임된 함수의 finally 블록을 실행하고, 쓰레드의 종료를 보장하려면 Join() 등을 호출하여 ThreadAbortException 예외를 잡을 수 있도록 해야 한다(즉, 쓰레드의 상태를 WaitSleepJoin으로 만들어서 예외를 잡을 수 있도록 한다). 여기서는 설명하지 않지만 가끔 쓰레드를 사용하다 보면 Win32Exception을 만나는 경우가 발생할 수 있다. 그러한 경우에는 System.ComponentModel 네임스페이스를 사용하도록 하고, Win32Exception 예외를 잡아서 처리하기 바란다. Win32Exception에는 일반 예외와 다른 몇 가지 메소드가 있는데, 이들은 CLR 런타임의 예외 코드를 Win32 예외 코드로 변환해준다. 요약 쓰레드의 예외 처리는 교착상태, 경쟁 조건 등과 같은 여러 가지 문제에 따라 다양한 예외처리가 있을 수 있지만, 여기서는 닷넷에서 지원하는 기본적인 예외처리를 살펴보았다. 쓰레드의 예외 처리는 예외의 종류에 따라 위임된 함수에서 처리해야 하는 예외와 쓰레드를 이용하는 함수에서 처리해야 하는 예외가 다르므로 주의하기 바란다. 또한, ThreadAbortException 예외는 쓰레드가 WaitSleepJoin 상태가 아닌 경우에는 예외를 잡을 수 없다는 것을 기억한다. 다음 단계 다음에는 UNIX나 LINUX에서 많이 보던 top과 비슷한 wintop을 C# 언어와 윈도우 폼으로 작성해 볼 것이다. 쓰레드 프로그래밍을 하다 보면 NT의 작업 관리자의 정보로는 부족한 경우가 있다. 때문에 보다 자세한 정보를 볼 수 있는 윈도우 버전의 top을 작성해 볼 것이다.
TAG :
댓글 입력
자료실

최근 본 책0