본문으로 바로가기

ThreadPool을 이용한 순차적 실행

category Development/C# 2009. 11. 24. 20:12

순차적으로 실행되어야 하는 일련의 작업이 있다고 하자.
이걸 어떻게 구현하면 될까?
이건 질문 자체가 어이없다. 그냥 차례대로 실행시키면 된다.

 //[SerialWorkProcessor 구현1]
 class SerialWorkProcessor 
 {
     private Queue queue; 
     ...

     public void Process()
     {
         while( this.queue.Count > 0 )
         {
              WorkItem item = this.queue.dequeue();
              item.Execute();
         }
     }
 }

만약 UI Thread가 있고 이 일련의 작업 처리가 UI Thread를 방해하지 않아야 하는 경우라 하더라도,
Thread를 하나 생성해서 실행시키는 방법으로 쉽게 해결할 수 있다.

  //[SerialWorkProcessor 구현2]
 class SerialWorkProcessor 
 {
     private Queue queue;
     ...

     public void Process()
     {
         Thread thread = new Thread( new ThreadStart( ProcessWorkItem ) );
         thread.Start();
     }

     private void ProcessWorkItem()
     {
         while( this.queue.Count > 0 )
         {
              WorkItem item = this.queue.dequeue();
              item.Execute();
         }
     }
 }
그러면 이렇게 순차적으로 실행되어야 하는 WorkItem 덩어리가 여러개인 경우는 어떤가?
이것도 그리 어렵지 않게 구현할 수 있을 것 같다. 위에서 보인 SerialWorkProcessor를 여러개 만들어서 실행시키면 되는 것이다.
그러면 각각의 SerialWorkProcessor 인스턴스가 Thread를 하나씩 만들어 자신에게 등록된 WorkItem을 차례대로 실행시킬 것이기 때문이다.
   
 SerialWorkProcess p1; 
 SerialWorkProcess p2;

 SerialWorkProcess p3;

 ...


 void ProcessAll()
 {
     p1.Process();
     p2.Process();
     p3.Process();
 }
그런데, 이런 SerialWorkProcessor가 몇백개쯤 생성되어야 한다면 어떤가?
만약 위의 방법을 그대로 사용하면 결국 Thread도 몇백개가 생성된다는 말이 된다.
몇백개의 Thread 생성쯤은 시스템이 충분히 감당할 수 있다고 하더라도, 시스템의 CPU 혹은 코어 수보다 훨씬 많은 Thread를 생성하는 것이 성능을 떨어뜨린다는 사실은, 이제는 상식수준에서 안다. 그럼 어떻게 해야 할까?

이렇게 Thread가 과도하게 생성되어야 하는 상황이면 떠오른 것이 ThreadPool이다. ThreadPool은 Thread가 과도하게 생성되어 성능이 나빠지는 것을 해결해 주는 좋은 솔루션이다.
위의 상황을 ThreadPool을 가지고 해결하는 방법은 의외로 간단할 수 있다. 앞서 SerialWorkProcessor에서 Thread를 생성하는 부분을 ThreadPool이 맡기는 코드로 바꾸기만 하면된다.

 //[SerialWorkProcessor 구현3]   
 class SerialWorkProcessor 
 {
     private Queue queue;
     ...
 
     public void Process()
     {
         ThreadPool.QueueUserWorkItem( new WaitCallback( ProcessWorkItem ) );
     }

     private void ProcessWorkItem( object ignore )
     {
         while( this.queue.Count > 0 )
         {
              WorkItem item = this.queue.dequeue();
              item.Execute();
         }
     }
 }
위와 같이 구현하면 SerialWorkProcessor가 여러개 생기더라도 Thread가 과도하게 생성되는 것을 막을 수 있다.
하지만 이 경우에도 전혀 문제가 없을까?

만약 ThreadPool이 10개의 Thread가 동시에 실행되도록 관리하고 있는 상황에서, SerialWorkProcessor를 100개 생성하여 실행하도록 했다고 해 보자.
그러면 처음 10개의 SerialWorkProcessor가 작업을 시작할 것이다. 하지만 나머지 90개의 SerialWorkProcessor는 어떤가?
나머지 90개의 SerialWorkProcessor는 처음 시작된 SerialWorkProcessor가 자신에게 등록된 WorkItem을 모두 끝마쳐야만,
겨우 첫번째 WorkItem을 시작할 수 있게 된다. 즉 한 SerialWorkProcessor의 모든 WorkItem을 마쳐야 다음 SerialWorkProcessor가 작업을 시작할 수 있게 되는 것이다.
ThreadPool을 사용한 목적이 과도한 Thread 사용을 막기위한 것이기 때문에, 나머지 90개의 SerialWorkProcessor가 블록되는 것은 어쩌면 당연한 현상이라고 생각할 수 있지만, 이건 너무 하다 싶지 않은가?

다시 말해 SerialWorkProcessor 간에 형평성에 문제가 있다는 것이다.

이 형평성 문제는 경우에 따라서는 크게 문제가 되지 않을 수도 있다.
하지만 모든 SerialWorkProcessor가 공평하게 자신의 WorkItem을 실행시킬 수 있도록 해야 하는 경우도 있을 것이다. 이 경우에는 어떻게 해야 하는가?

다음의 코드를 보자.

 //[SerialWorkProcessor 구현4]
 class SerialWorkProcessor 
 {
     private Queue queue;
     ...

     public void Process()
     {
         ThreadPool.QueueUserWorkItem( new WaitCallback( ProcessWorkItem ) );
     }

     private void ProcessWorkItem( object ignore )
     {
         if( this.queue.Count <= 0 )
             return;

         WorkItem item = this.queue.dequeue();
         item.Execute();
         
         ThreadPool.QueueUserWorkItem( new WaitCallback( ProcessWorkItem ) );
     }
 }

위에서 코드에서 그전 코드와 달라진 점은 ProcessWorkItem 메소드에서 while 문을 없애고 하나의 WorkItem만 실행하도록 했다. 그리고 WorkItem 하나를 실행한 후에는 자기 자신(ProcessWorkItem메소드)을 다시 ThreadPool에 실행되도록 등록하는 것이다.

이렇게 하는 것은 어떤 의미가 있을까?

위의 코드는 바로 SerialWorkProcessor에 등록된 모든 WorkItme을 하나의 처리 덩어리로 ThreadPool에 등록하는 것이 아니라,
WorkItem 하나를 하나의 처리 덩어리로 ThreadPool에 등록하는 것이다.
SerialWorProcessor는 자신의 WorkItem이 차례로 하나씩 실행되는 것을 보장받으면서, ThreadPool은 좀더 작은 WorkItem 단위로 Thread를 배분할 수 있게 되는 것이다.
이렇게 하면, 여러개의 SerialWorkProcessor가 있는 상황에서도 각 SerialWorkProcessor간에 어느정도 형평성을 줄 수 있게 된다.
또한 한 SerialWorkProcessor에 실행이 오래 걸리는 WorkItem이 있더라도 다른 SerialWorkProcessor의 실행이 지연되는 상황을 어느정도 만회할 수 있게된다.

물론 위의 코드는 WorkItem 하나를 실행시킬때 마다 매번 ThreadPool에 등록하고 다시 실행되는 오버헤드가 발생하기 때문에 만능 해결책을 아니다. WorkItem 하나의 실행이 아주 짧은 경우는 오히려 그 위에 제시한 구현들이 더 성능이 좋게 나올 수도 있다.

어떤 것을 사용해야 하는 지는, 전적으로 해결해야 하는 상황과 프로그래머의 판단이 결정한다는 것은 두말하면 잔소리다.

'Development > C#' 카테고리의 다른 글

닷넷 프레임워크 기반의 소켓 프로그래밍  (0) 2009.11.27
C# 데이터 형식  (0) 2009.11.26
Threading.Timer  (0) 2009.11.17
const 와 readonly 그리고 enum  (0) 2009.11.10
MS Chart Control  (2) 2009.11.03