C# Threading 관련 기술

Thread

  • System.Threading 네임스페이스에 정의되어 있다
  • .NET Framework 1.0부터 사용 가능하다
  • Thread에 실행될 로직을 넘겨주며, 생성한 후 명시적으로 실행 해야 한다.
  • Thread는, 생성, 삭제될 때 상대적으로 많은 시스템 자원을 사용해 비효율적이다.
    ThreadPool 은 Thread 를 미리 생성해 두고, 필요할 때 사용하고 반환하는 방식으로 기존 Thread 를 개선했다.

ThreadPool 

  • System.Threading 네임스페이스에 정의되어 있다
  • .NET Framework 1.0부터 사용 가능하다
  • ThreadPool은 특정 이름을 지정할 수 없다.
    Join 과 같은 조정을 사용할 수 없다.
    백그라운드 스레드라서 의도하지 않게 종료될 수 있다.

    Task는 내부적으로는 ThreadPool을 사용하고, ThreadPool의 여러 가지 단점을 보완했다.

Task

  • System.Threading.Tasks 네임스페이스에 정의되어 있다
  • .NET Framework 4.0부터 사용 가능하다.
  • 쓰레드풀로부터 쓰레드를 가져와 비동기 작업을 실행한다.
  • Task 관련 클래스들과 Parallel 클래스들을 합쳐 Task Parallel Library(TPL)이라 부른다.
  • 이들은 기본적으로 다중 CPU 병렬 처리를 위해 만들어졌다.

async, await

  • .NET Framework 5.0부터 사용 가능하다.
  • Task 키워드와 잘 호환된다.

 

 

Thread 문제점

기본적인 스레드 사용법에는 몇가지 문제가 있다.

스레드간의 컨텍스트 스위칭 오버헤드 (context switching overhead)가 발생한다.

스레드의 생성과 제거에도 오버헤드가 발생한다.

스레드가 작업을 수행하지 않더라도 존재 자체만으로 여전히 리소스를 소비하고 오버헤드를 생성한다.

항상 최대 갯수의 스레드가 동작할 필요가 없다.

 

 

해결책

스레드를 요청마다 만들고 삭제하며 자원을 낭비하는 것보다, 적당한 개수의 스레드를 미리 만들어 놓고 필요한 경우 사용하고 되돌려 주는 방식인 ThreadPool이 등장했다.

 

 

ThreadPool

동작 방식

  • 지정된 갯수를 스레드를 스레드 풀에 미리 생성한다.
  • 스레드 실행 할 request(요청)는 스레드 풀의 Queue(큐)에 넣어 관리한다.
  • 사용 가능한 스레드가 없으면, 요청은 큐에 대기하게 된다.
  • 사용되던 스레드가 작업을 끝내고 스레드 풀의 대기상태로 돌아오면, 큐에 대기 중인 요청에 재사용된다.

 

 

유의 사항

  • 서버와 같은 다수의 요청 작업을 처리해야 하는 경우,
    최대 스레드 갯수와, 요청을 저장할 큐의 크기를 제어하는 것이 중요하다.
  • 무한정 요청 정보를 저장할 수 없고, 사용하지 않을 스레드를 미리 많이 만들어 둘 이유도 없다.  
  • 그래서 ThreadPool은 작업의 완료 여부를 알 필요 없는 짧은 작업을 처리하는데 주로 사용된다.

 

① 기본 사용법

 

  1. MSDN
  2. C# ThreadPool 클래스
    • public static class ThreadPool
  3. 스레딩하고자 하는 함수를 동작
    1. ThreadPool.QueueUserWorkItem(함수이름) 
      Thread의 start( ) 역할
    2. 해당 함수는 매개변수로 object 혹은 object? 클래스형
      using System.Threading;
      //... Queue the task.
      ThreadPool.QueueUserWorkItem(Func);
  4. 스레드 제한
    1. ThreadPool.SetMinThreads(스레드 개수, 비동기 I/O Thread 개수) 
      최소로 빌려올 수 있는 스레드 개수
    2. ThreadPool.SetMaxThreads(스레드 개수, 비동기 I/O Thread 개수)
      최대로 빌려올 수 있는 스레드 개수
      // 쓰레드를 최소 1개 최대 3개까지밖에 못빌려주도록 제한
      ThreadPool.SetMinThreads(1, 1);
      ThreadPool.SetMaxThreads(3, 3)
      
      // 이미 3개의 스레드가 실행되고 있기 때문에 4번째 스레드를 생성하지 못한다. 
      // 이를 개선하고자 Task 등장​

 

③ 예제 

스레드풀 실행

class Program
{
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem(Run); // cnt=null
        ThreadPool.QueueUserWorkItem(Run, 10); // cnt=10
        ThreadPool.QueueUserWorkItem(Run, 20);

        Console.WriteLine("Press Any key...");
        Console.ReadLine();
    }

    static void Run(object cnt)
    {
        if (cnt == null) 
            return;

        int num = (int)cnt;
        Console.WriteLine("cnt={0}", num);
    }
}

 

 

스레드풀의 문제 1

  • Thread Pool에 남아있는 자원이 없다면 더 이상 스레드를 생성할 수 없다.
  • Thread Pool은 자원 공유 → 대기 → 반환 → 공유 → 대기 → 반환을 반복한다.
  • 아래와 같은 경우에선 이미 3개의 스레드가 실행되고 있기 때문에 4번째 스레드를 생성하지 못한다.
  • 이를 해결하는 방법으로 Task를 사용
using System.Threading;

public class Program
{
    // 4번째 스레드로 실행 시도할 함수
    static void Run(object? obj)
    {
        for (int i = 0; i < 5; i++)
            Console.WriteLine("Create Thread");
    }
    
    static void Main(string[] args)
    {
        // 스레드 풀의 스레드를 최소 1개 최대 3개까지 제한
        ThreadPool.SetMinThreads(1, 1);
        ThreadPool.SetMaxThreads(3, 3);

        // 스레드 풀에서 스레드를 3개 가져와 무한 실행
        for (int i = 0; i < 3; i++)
            
            ThreadPool.QueueUserWorkItem((obj) =>
            {   // 스레드 ID 확인 
                int threadID = Thread.CurrentThread.ManagedThreadId;

                while (true) {
                    Console.WriteLine($"스레드ID {threadID} 카운트: {i}");
                    Thread.Sleep(1001);
                }; 
            });

        // 4번째 스레드 사용 시도하지만, 남은 스레드가 없음
        ThreadPool.QueueUserWorkItem(Run);

        while (true) ;
    }
}

 

스레드풀의 제한 

  • 스레드풀의 스레드 개수를 2개로 제한했다.
  • Thread Pool은 자원 공유 → 대기 → 반환 → 공유 → 대기 → 반환을 반복한다.
  • 대기 상태의 스레드가 있을 때 스레드가 동작한다.
using System;
using System.Threading;

namespace ThreadTest
{
    // 스레드로 전달 될 파라미터 클래스
    class Node
    {
        // 콘솔 출력시 사용되는 텍스트
        public string? Text { get; set; }
        // 반복문 횟수
        public int Count { get; set; }
        // Sleep의 시간틱
        public int Tick { get; set; }
    }
    
    class Program
    {
        // 스레드로 실행 될 함수
        static void ThreadProc(Object callBack)
        {
            // 파라미터 값이 Node 클래스가 아니면 종료
            if (callBack.GetType() != typeof(Node))
                return;

            // Node 타입으로 강제 캐스트(자료형이 Object 타입)
            var node = (Node)callBack;

            // 설정된 반복문의 횟수만큼
            for (int i = 0; i < node.Count; i++)
            {
                // 콘솔 출력
                Console.WriteLine(node.Text + " = " + i);
                // 설정된 Sleep 지연
                Thread.Sleep(node.Tick);
            }

            // 완료 콘솔 출력
            Console.WriteLine("Complete " + node.Text);
        }

        static void Main(string[] args)
        {
            // ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
            if (ThreadPool.SetMinThreads(0, 0) && ThreadPool.SetMaxThreads(2, 2))
            {
                // ThreadPool, ThreadProc 함수에, Node 객체를 매개변수로 넘긴다.
                ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "A", Count = 3, Tick = 1000 });
                ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "B", Count = 5, Tick = 10 });
                ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "C", Count = 2, Tick = 500 });
                ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "D", Count = 7, Tick = 300 });
                ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "E", Count = 4, Tick = 200 });
            }

            // 아무 키나 누르면 종료
            Console.WriteLine("Press Any key...");
            Console.ReadLine();
        }
    }
}

 

스레드풀의 문제 2

위 소스코드에서 마지막 라인의 Console.ReadLine(); 을 제거하면, 프로그램은 바로 종료된다.
기본적으로 Join 과 같은 기능을 제공하지 않는다.

스레드풀은 백그라운드 스레드라서 의도하지 않게 종료될 수 있다.