In to the C# 2014. 1. 14. 14:09

Thread Pool과 Task

C#에서 Task는 비동기 작업을 나타낸다고 합니다.

Task는 .NET Framework4.0 부터 사용 할 수 있습니다. Task의 작동방식은 .NET Framework에서 관리되고 있는 ThreadPool에서 작동합니다. 그럼 ThreadPooling에 대해서 간단히 알아 보겠습니다.

닷넷 프레임워크로 작성된 어플리케이션은 실행되면서 자동으로 ThreadPool도 생성이 됩니다. ThreadPool을 사용하는 Task와 비동기 타이머 같은 것들을 지원하기 위해서죠. 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace ConsoleApplication26
{
  class Program
  {
    static void Main(string[] args)
    {
      int minWorkerThreads = 0;
            int minCompletionPortThreads = 0;
            ThreadPool.GetMinThreads(out minWorkerThreads, out minCompletionPortThreads); 
 
            int maxWorkerThreads = 0;
            int maxCompletionPortThreads = 0;
            ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads); 
 
            Console.WriteLine("Min Worker Threads : {0}, max Worker Threads : {1}", minWorkerThreads, maxWorkerThreads);
            Console.WriteLine("Min CompletionPort Threads : {0}, max CompletionPort Threads : {1}", minCompletionPortThreads, maxCompletionPortThreads);
        }
    }
}


위 코드는 ThreadPool의 최소, 최대 Thread 개수를 구합니다. 실행하면 아래와 같은 결과가 나옵니다.



ThreadPool은 최소 4개부터 최대 1023개까지 작업자 스레드를 갖을 수 있습니다.

CompletionPort Thread는 비동기 I/O스레드의 수 입니다. 비동기 I/O스레드의 수는 4 ~ 1000개 까지 갖을 수 있습니다.


ThreadPool은 언덕등반 알고리즘(Hill Climb Algorithm)에 의해 자동으로 스레드가 생성이 됩니다.

즉, Task를 쓸 경우 Thread의 개수는 닷넷프레임워크가 알아서 조절해 준다는 것이지요.

간단한 Task를 만들어서 ThreadPool이 어떻게 작동하는지 보겠습니다.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication26
{
    class Program
    {
        static List<int> threadIdList = new List<int>();
        static object lockList = new object();

        static void AddBag(int threadId)
        {
            lock (lockList)
            {
                if (!threadIdList.Contains(threadId))
                    threadIdList.Add(threadId);
            }
        }

        static void Print()
        {
            Console.WriteLine("Thread Count : {0}, Thread IDs : {1}", threadIdList.Count, string.Join(",", threadIdList));
        }

        static void Main(string[] args)
        {
            List<Task> taskList = new List<Task>();
            for (int i = 0; i < 100; i++)
            {
                Task t = new Task(() =>
                {
                    AddBag(Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(1000);
                });

                taskList.Add(t);
                t.Start();

            }
            taskList.ForEach(x => x.Wait());
            Print();
        }
    }
}


Main 메소드에서 스레드에 sleep을 1초 주었습니다.

처음엔 4개의 thread에서 Task의 작업을 수행하게 됩니다. 그런데 sleep(=부하)가 일어나다 보니 닷넷 프레임워크에선 수행속도가 늦어짐을 감지하고 ThreadPool에서 Thread를 더 추가해 줍니다. 위의 결과는 아래와 같습니다. 최종적인 결과로는 사용한 Thread의 개수는 9개 입니다.



아마도 Thread.Sleep 메소드가 소스에서 제거가 된다면 Thread는 4개만 돌아갈 것입니다. Task를 수행하는데 delay가 없기 때문이지요.


여기서 전 좀 더 많은 스레드를 생성해도 성능상 문제가 없을 텐데라는 생각이 들면서, 좀 더 쉽고, 좀 더 간단하게(직접 ThreadPool을 구현하지 않아도 되는!)Thread의 수를 늘릴 방법을 찾았습니다.


처음에 어느 정도의 스레드의 수를 할당해 놓으면 Task가 delay되는 것을 감지하고 thread를 할당해주는 작업이 생기지 않을 것이기 때문입니다.


그럼 최소 스레드의 개수를 늘려 보겠습니다.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication26
{
    class Program
    {
        static List<int> threadIdList = new List<int>();
        static object lockList = new object();

        static void AddBag(int threadId)
        {
            lock (lockList)
            {
                if (!threadIdList.Contains(threadId))
                    threadIdList.Add(threadId);
            }
        }

        static void Print()
        {
            Console.WriteLine("Thread Count : {0}, Thread IDs : {1}", threadIdList.Count, string.Join(",", threadIdList));
        }

        static void Main(string[] args)
        {
            ThreadPool.SetMinThreads(50, 100);
            List<Task> taskList = new List<Task>();
            for (int i = 0; i < 100; i++)
            {
                Task t = new Task(() =>
                {
                    AddBag(Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(1000);
                });

                taskList.Add(t);
                t.Start();

            }
            taskList.ForEach(x => x.Wait());
            Print();
        }
    }
}


메인 메소드에 ThreadPool.SetMinThreads를 이용하여 최소 워커 스레드를 50개로 설정하였습니다.

결과는 아래와 같습니다.



Thread Count가 50개가 되었습니다.

ThreadPool 설정이 Task가 작동하는데 영향이 있다는 것을 확인하였습니다.

ThreadPool 의 개수는 적절히 조절을 해야 합니다. 너무 많은 수는 어플리케이션의 성능을 떨어뜨리니 주의 하는 것이 좋습니다.


아! 마지막으로 중요한 것!

Task 수행은 위에도 설명하였듯이 ThreadPool에서 해당 Task가 작업을 하게 됩니다. 그런데 만약 여기에서 Sleep을 걸게 되면 안됩니다. 왜냐면 전자 Task가 수행되는 Thread는 후자 Task가 대기 할 수도 있기 때문입니다. 후자 Task는 전자 Task가 Sleep을 하고 있기 때문에 기다리다가 다른 Thread로 갈아탑니다. 이렇게 되면 보이지도 않게 퍼포먼스가 떨어지게 됩니다.


즉, 이말은 Task를 수행한다는 것은 최악의 상황일 때 즉시 실행된다는 것을 보장하지 못한다는 것입니다. 정말 성능적으로 중요한 백그라운드 작업이라면 Task를 사용하지 말고 Thread를 생성해서 사용하여야 합니다.


끝~