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

한빛출판네트워크

IT/모바일

C# 쓰레드 이야기: 3. 쓰레드 제어

한빛미디어

|

2001-11-15

|

by HANBIT

46,332

편집자주: 모든 코드는 .NET Framework beta2(1.0.2419)와 .NET Framework RC(1.0.3328.4)에서 테스트되었다. 지난 시간(2. 다중 쓰레드)에는 여러개의 쓰레드를 생성하는 방법에 대해서 간단히 살펴보았다. 생각처럼 어렵지 않았을 것이다. 단순히 여러개의 쓰레드를 생성하고 각각의 함수를 위임하고 실행하면 그만이었을 것이다. 쓰레드 기다리기(Joining Thread) 일반적인 쓰레드 처리는 코드의 수행과 상관없이 계속해서 실행된다. 만약, 여러분이 쓰레드를 이용해서 긴 시간 동안 처리해야 하는 작업을 맡았고, 그 작업이 끝나면 결과를 받아서 화면에 출력을 하거나 계산을 하는 프로그램을 작성한다고 생각해보자. 이 경우에 쓰레드에서 처리하고 있는 작업이 끝나기도 전에 프로그램은 이미 출력이나 계산을 수행하는 코드를 실행하게 된다(아마도 에러를 만나게 될 것이다). 이러한 경우에 화면에 출력하거나 계산을 수행하기 전에 쓰레드가 끝나는 것을 기다려야 한다.
C# 에센스
먼저, 쓰레드를 기다리지 않을 경우에 어떤 일이 일어나는지 알아보기 위해 지난 시간에 사용했던 코드를 이용해 보도록 하자. 지난 시간에 사용한 코드에서 다음부분을 찾아서 수정한다(빨간색으로 표시된 부분).
private void DoTest()
{
  Thread[] aThread =
  {
    new Thread( new ThreadStart(DoPrinting) ),
    new Thread( new ThreadStart(DoSpelling) ),
    new Thread( new ThreadStart(DoSaving) )
  };

  foreach( Thread t in aThread)
  {
    t.Start();
  }

  aThread[1].Interrupt();

  Console.WriteLine("모든 쓰레드가 종료되었습니다");
}
또한 모든 Do 함수에서
Thread.Sleep(50);
과 같이 모두 같은 시간동안 수행되도록 한다. 코드를 다시 컴파일하고 실행하면 다음과 같은 결과가 나타날 것이다.

코드가 쓰레드로 수행되고 있기 때문에 실제로 모든 쓰레드가 종료되지 않았는데 "모든 쓰레드가 종료되었습니다"라는 메시지가 중간에 나타나는 것을 알 수 있을 것이다. 모든 쓰레드가 끝난 다음에 "모든 쓰레드가 종료되었습니다"라고 나타나도록 하려면 다음과 같이 코드를 변경하면 된다.
foreach( Thread t in aThread)
{
  t.Start();
}

  aThread[0].Join();	// 인쇄 작업
  aThread[1].Join();	// 철자 검사
  aThread[2].Join();	// 저장 하기

  Console.WriteLine("모든 쓰레드가 종료되었습니다");
}
foreach 루프문에서 Join()을 수행하지 않고, 바깥에서 쓰레드의 배열에서 직접 Join()을 수행한다는 것에 주의하기 바란다. 각각의 쓰레드는 인쇄 작업, 철자 검사, 저장 하기를 수행한다. 또한 모든 쓰레드가 종료될 때까지 Console.WriteLine()은 수행되지 않는다.

결과에서 알 수 있는 것처럼 모든 쓰레드가 끝난 다음에 Console.WriteLine()이 실행되는 것을 알 수 있다. 이와 같이 각각의 쓰레드가 독립적으로 처리되고, 모든 쓰레드가 종료된 다음에 특정 코드를 실행하기 위해서 위와 같이 Join()을 사용할 수 있다. 쓰레드 t1과 t2가 각각 작업을 수행하고 있을 때,
t2.Join();
과 같은 코드를 넣으면, 쓰레드 t1에서 수행중인 함수의 실행을 중지하고, 쓰레드 t2가 종료될 때 까지 실행을 기다리게 된다. 위에서는 aThread[0], aThread[1], aThread[2]에 대해서 각각 Join()을 호출하였다. 각 쓰레드 aThread[0], aThread[1], aThread[2]는 응용 프로그램 쓰레드에 대해서 Join()을 수행한다. 따라서 응용 프로그램 쓰레드는 실행을 중지하고 각각의 aThread[0], aThread[1], aThread[2]의 실행이 종료될 때까지 기다리게 된다. 다시 정리하면 다음과 같다.
aThread[0].Join();  // 응용 프로그램 쓰레드 정지하고 인쇄 쓰레드 완료를 기다림
aThread[1].Join();  // 응용 프로그램 쓰레드 정지하고 철자 쓰레드 완료를 기다림
aThread[2].Join();  // 응용 프로그램 쓰레드 정지하고 저장 쓰레드 완료를 기다림
첫번째 시간에 응용 프로그램의 프로세스와 쓰레드에 대해서 이야기 한 것을 기억하는가? 기억하지 못하는 독자를 위해서 잠시 기억을 되살려 보도록 하자. 응용 프로그램은 하나의 프로세스를 가진다. 이 프로세스는 실제로 하나의 쓰레드이며, 프로세스는 어드레스 공간에 대한 단순한 주소라고 이야기 했었다. 또한 프로세스는 단순히 쓰레드에 대한 컨테이너 역할을 한다고 얘기했었다. 다시 말해 각각의 aThread[].Join()은 응용 프로그램 쓰레드 Main()에 대한 Join()이라는 것을 알 수 있을 것이다. 여기서는 모든 쓰레드가 응용 프로그램 쓰레드에 대해서 Join()을 했으므로 모든 쓰레드가 종료될 때까지 응용 프로그램 쓰레드 Main()을 실행을 중지하고 기다린다. 반대로 aThread[0].Join();을 지우고, DoPrinting() 함수에서 Thread.Sleep(100);으로 다시 컴파일하여 실행해보면 다음과 같은 결과를 볼 수 있을 것이다.

결과에서 알 수 있는 것처럼 철자 검사 aThread[1]과 저장 aThread[2]가 끝나고 "모든 쓰레드가 종료되었습니다"라는 메시지를 출력한 다음에도 계속해서 aThread[0]은 실행되는 것을 알 수 있다. 이렇게 실행되는 이유는 aThread[1]과 aThread[2]가 Join()을 수행하여 각각의 쓰레드가 종료 될 때 까지 응용 프로그램 쓰레드를 중지시키고 대기 상태로 두는 반면에 인쇄 작업을 수행하는 aThread[0]은 응용 프로그램 쓰레드에 관계없이 동시에 실행되기 때문이다. Join에서 주의할 점 위에서 Join이 어떻게 작동하는 가에 대해서 자세하게 설명했다(어쩌면 독자들의 머리를 아프게 했을지도 모르겠다). 여러분은 여러 개의 쓰레드를 foreach를 사용하여 멋지게 처리하는 것을 보았는데, 왜 루프안에 Join()을 넣지 않았는지 의아해 했을지도 모르겠다. "왜 루프안에서 Join()을 수행하지 않고, 루프 밖에서 일일이 수행하는 거지?" 이제, 이것에 대해서 설명하고자 한다. 먼저 다음과 같이 코드를 변경해 보도록 하자.
foreach( Thread t in aThread)
{
  t.Start();
  t.Join();
}

  Console.WriteLine("모든 쓰레드가 종료되었습니다");
}
여러분이 생각하는 것처럼 foreach 루프안에서 일괄적으로 쓰레드를 시작하고 Join()을 수행하고 있다. 코드를 다시 컴파일하고 결과를 살펴보면 다음과 같은 결과가 나타날 것이다.

결과에서 알 수 있는 것처럼 쓰레드로 동시에 처리되는 것이 아니라 순차적으로 실행되는 것을 알 수 있다. 루프안에서 Start()에 의해서 쓰레드가 시작되고, Join()에 의해서 응용 프로그램 쓰레드에 대해서 Join()을 수행하게 된다. 다시 말해, 응용 프로그램은 t.Join();까지 실행한 다음에 쓰레드 t의 수행이 끝날 때까지 모든 작업을 중지하고 기다리게 된다. 마찬가지로 두 번째 루프를 처리할 때도 같은 방식으로 동작한다. 때문에 쓰레드 프로그래밍을 하는 경우에는 이와 같이 루프안에서 Join()을 사용해서는 안된다. 쓰레드에 대해서 Join()을 수행하고 싶다면 다음과 같은 방식으로 코딩하기 바란다.
foreach( Thread t in aThread)
{
  t.Start();
}

foreach( Thread t in aThread)
{
  t.Join();
}
쓰레드를 시작하는 부분과 쓰레드를 Join()하는 부분을 각각 두 개의 루프로 분리해서 처리하도록 한다. Join 대기하기 먼저 각각의 쓰레드 t1, t2, t3가 있다고 하자. 다음과 같이 모두 Join()을 수행하고 있다고 가정하자.
t1.Join();
t2.Join();
t3.Join();
이 세가지 쓰레드는 각각의 쓰레드가 종료되고 제어권을 다시 넘길 때까지 응용 프로그램 쓰레드를 중지시킨다. 만약에 이 쓰레드 중에 어떤 쓰레드가 종료되지 않는다면 응용 프로그램은 영원히 종료되지 않을 수도 있다. 이러한 경우에 일정 시간 동안만 응용 프로그램 쓰레드를 대기 시키도록 할 수 있다. 사용법은 다음과 같다.
t1.Join(100);  // 100ms 동안만 응용 프로그램 쓰레드를 중지시킨다.
t2.Join(100);
t3.Join(100);
이제 앞에서 살펴본 예제에서 각각의 쓰레드를 다음과 같이 변경하도록 하자.
foreach( Thread t in aThread)
{
  t.Start();
}

aThread[0].Join(100);
aThread[1].Join(100);
aThread[2].Join(100);
이와 같이 코드를 변경한 다음에 실행다면 다음과 같은 결과를 확인할 수 있을 것이다.

결과에서 알 수 있는 것처럼 각각의 쓰레드가 Join(100)에서 지정했으므로 300ms 초 이후에 "모든 쓰레드가 종료되었습니다"라는 메시지가 출력된 후에 계속해서 쓰레드가 수행되는 것을 알 수 있다. 각각의 쓰레드에 대해서 Join(1000)으로 변경한다면 결과는 다음과 같을 것이다.

각각의 쓰레드에 대해서 Join(1000)으로 변경했으므로 3000ms 이후에 응용 프로그램 쓰레드에 제어권이 넘어가게 되고, "모든 쓰레드가 종료되었습니다"라는 메시지가 출력되는 것을 알 수 있을 것이다. 앞의 예제에서 각각의 쓰레드에 대해서 Join(1000), Join(2000)과 같이 값을 바꿔보면서 여러가지로 테스트 해 보면 Join()에 대해서 더 잘 이해할 수 있을 것이다. 쓰레드 정리 지난 시간에 쓰레드를 종료하기 위해 Interrupt()를 사용했던 것을 기억하는지 모르겠다. 닷넷 환경은 CLR에 의해서 쓰레기 수집(Garbage Collection)을 하기 때문에 프로그래머가 쓰레드를 직접 정리하지 않아도 된다. 정리 마지막으로 쓰레드에 대해서 정리해 보도록 하자. 위 예제에서는 3개의 쓰레드를 사용했다. 그렇다면 실제로 응용 프로그램은 몇 개의 쓰레드를 사용했는가? 답은 "4개의 쓰레드를 사용했다"이다. 이 한 개는 응용 프로그램에 대한 쓰레드가 되며, 또한 하나의 프로세스를 사용한다. 이 프로세스는 4개의 쓰레드에 대한 컨테이너가 된다. 응용 프로그램 쓰레드를 1차 쓰레드라고 하며, 예제에서 생성한 각각의 쓰레드들은 모두 2차 쓰레드라고 한다. 따라서 2차 쓰레드에 대한 제어권은 1차 쓰레드가 갖고 있다. 벌써 머리가 복잡해 지는가? 이제 잠시 쉬면서 다음을 위한 휴식을 하도록 하자. 다음 단계 지금까지 쓰레드에 대해 알아보면서 쓰레드의 처리를 제어할 수 있는 것들을 알아봤다. 첫번째는 Thread.Sleep()을 이용해서 쓰레드의 처리를 단순히 지연시키는 것에서부터, 쓰레드의 처리가 종료될 때까지 선행 쓰레드를 멈추게 하는 Join()에 대해서 알아봤으며, 쓰레드를 종료 시킬 수 있는 Interrupted()에 대해서 알아보았다. 다음에는 1차 쓰레드와 2차 쓰레드에 대해서, 그리고 System.Threading 네임 스페이스와 System.Threading.Thread 클래스에 대해서 자세히 살펴보는 시간을 갖도록 하자. 아무쪼록 다음 시간까지 즐거운 탐험의 시간이 되기를 바란다. ☞ 소스 다운로드
TAG :
댓글 입력
자료실

최근 본 책0