멈추지 않고 끈질기게

[C#] Thread, ThreadPool, Task 본문

C#

[C#] Thread, ThreadPool, Task

sam0308 2023. 10. 26. 17:19

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

※ 해당 포스팅은 .Net 7.0 버전을 기준으로 작성되었습니다.

 

 

 이번 포스팅에서는 C#에서 스레드를 생성하는 방법에 대해 알아보겠습니다.

1. Thread 

 C#에서는 기본적으로 Thread 클래스를 통해 작업을 스레드로 실행할 수 있습니다.

class Program
{
    static void ThreadTest()
    {
        for(int i = 0; i < 10; i++)
            Console.WriteLine("Thread 실행");
    }

    static void Main(string[] args)
    {
        Console.WriteLine("시작");
        // ThreadPool 설정
        Thread t = new Thread(ThreadTest);
        t.Start();
        t.Join();
        Console.WriteLine("종료");
    }
}

사진 1. Thread

 Thread 객체를 생성하면서 생성자로 실행할 내용을 등록할 수 있습니다. 등록된 작업은 Start() 함수를 통해 실행하며, Join() 함수를 통해 작업이 완료될 때까지 메인 스레드를 대기시킬 수 있습니다.

 

 

2. ThreadPool

 C#에서는 Thread 외에도 스레드를 직접 관리하지 않고 사용할 수 있는 ThreadPool 클래스를 제공합니다.

class Program
{
    static void ThreadTest(object obj)
    {
        for(int i = 0; i < 10; i++)
            Console.WriteLine("Thread 실행");
    }

    static void Main(string[] args)
    {
        Console.WriteLine("시작");
        // Thread 생성
        ThreadPool.QueueUserWorkItem(ThreadTest);

        Console.WriteLine("종료");
    }
}

사진 2. ThreadPool

 

 ThreadPool 클래스의 QueueUserWorkItem() 함수를 통해 작업을 등록하여 스레드로 실행할 수 있습니다. 단, object 타입의 매개변수를 받는 함수를 등록해야 하므로 ThreadTest()에 매개변수를 추가했습니다. Thread 객체를 생성하고 실행하는 방법에 비해 코드가 단순함을 확인할 수 있습니다. 다만 등록한 작업은 자동으로 실행되고, Thread.Join()과 같은 종료시점까지 기다리는 수단이 없다는 점에 주의해야 합니다. 사진 2를 보면 메인스레드의 작업이 끝나고 실행된 모습을 확인할 수 있습니다. 

 

 또한 ThreadPool은 SetMinThreads(), SetMaxThreads()와 같이 ThreadPool이 관리할 스레드의 최소, 최대 갯수를 설정하는 함수를 제공합니다. 실행되는 각각의 스레드에 대한 추적, 관리는 어려운 대신, 여러개의 스레드를 생성, 관리하기에 적합합니다. 

 

3.Task

 Task는 Thread 클래스와 유사하게 사용할 수 있는 스레드 생성, 실행 클래스입니다.

class Program
{
    static void ThreadTest()
    {
        for(int i = 0; i < 10; i++)
            Console.WriteLine("Thread 실행");
    }

    static void Main(string[] args)
    {
        Console.WriteLine("시작");
        // Task 생성
        Task t = new Task(ThreadTest);
        t.Start();
        t.Wait();
        Console.WriteLine("종료");
    }
}

사진 3. Task

 코드를 보면 Thread 예시와 비교해서 Join() 대신 Wait() 함수를 사용하는 점 외에 똑같다는 것을 알 수 있습니다. 직접 Task 객체를 생성하여 스레드를 생성하고, 해당 객체를 통해 작업 시작 및 종료까지 대기 등의 기능을 사용할 수 있습니다. 

 

 Task와 Thread와의 차이점은 Task로 생성한 스레드는 기본적으로 ThreadPool에 포함된다는 것입니다. 

class Program
{
    static void ThreadTest()
    {
        for(int i = 0; i < 10; i++)
            Console.WriteLine("Thread 실행");
    }

    static void Main(string[] args)
    {
        Console.WriteLine("시작");
        // ThreadPool 설정
        ThreadPool.SetMinThreads(1, 1);
        ThreadPool.SetMaxThreads(3, 3);
        for(int i = 0; i < 3; i++)
            ThreadPool.QueueUserWorkItem((obj) => { while (true) { } });
        // Task 생성
        Task t = new Task(ThreadTest);
        t.Start();
        t.Wait();
        Console.WriteLine("종료");
    }
}

사진 4. Task 생성(일반)

 위 코드에서는 ThreadPool의 최소 스레드를 1, 최대 스레드를 3으로 설정한 뒤 무한 루프를 도는 작업 3개를 등록했습니다. 이렇게 ThreadPool을 포화상태로 만든 뒤 Task를 만들어 실행하고 Wait()으로 종료를 기다려보면, Task로 등록한 작업이 실행되지 않고 무한정 대기하는 모습을 확인할 수 있습니다. 이를 통해 기본적으로 Task를 통해 생성한 스레드가 ThreadPool에 포함된다는 것을 알 수 있습니다.

 

 다만 Task 생성 시, 생성자의 2번째 인자로 TaskCreationOption을 설정해줄 수 있습니다. 위 예시에서 해당 옵션을 LongRunning으로 세팅해 줄 경우, ThreadPool이 포화상태여도 Task가 정상적으로 실행됩니다.

class Program
{
    static void ThreadTest()
    {
        for(int i = 0; i < 10; i++)
            Console.WriteLine("Thread 실행");
    }

    static void Main(string[] args)
    {
        Console.WriteLine("시작");
        // ThreadPool 설정
        ThreadPool.SetMinThreads(1, 1);
        ThreadPool.SetMaxThreads(3, 3);
        for (int i = 0; i < 3; i++)
            ThreadPool.QueueUserWorkItem((obj) => { while (true) { } });
        // Task 생성(LongRunning)
        Task t = new Task(ThreadTest, TaskCreationOptions.LongRunning);
        t.Start();
        t.Wait();
        Console.WriteLine("종료");
    }
}

사진 5. Task 생성(LongRunning)

 사진 4의 예시와 동일한 코드에서 TaskCreationOptions.LongRunning을 추가했더니 정상적으로 실행되는 모습입니다. 해당 옵션은 이 Task가 시간을 상당히 소요할 것이라고 알림으로서, ThreadPool의 제한을 넘어서 추가로 스레드를 생성할 수 있도록 해줍니다.


 Thread는 직접 객체를 생성함으로서 해당 작업의 추적, 관리는 용이하지만, 매 작업마다 Thread 객체를 생성해야 하므로 전체 스레드들의 관리는 어려운 면이 있습니다. ThreadPool을 이용할 경우 전체 스레드들의 관리는 용이하지만, 반대로 각 스레드에 대한 추적, 관리는 어렵습니다.

 

 Task는 Thread와 마찬가지로 직접 객체를 생성하여 사용하므로 해당 작업에 대한 추적, 관리가 가능하며, 동시에 ThreadPool에 포함되기 때문에 ThreadPool을 통한 전체 스레드들의 관리에도 영향을 받습니다. 비교적 가벼운 작업들은 ThreadPool에 바로 등록하여 실행하고, 중요한 작업은 Task 객체를 생성하여 실행함으로서 개별 추적, 관리가 가능하도록 하는 것이 좋을 듯 싶습니다.