C#에서 멀티쓰레드를 생성하고 이를 사용하는 방법을 알아보겠습니다.
C#에서는 대표적으로 3개의 방법이 있는데 각각의 방법과 사용예제를 한번 살펴보겠습니다.
1) Thread
Thread 스레드이름 = new Thread(수행함수);
위와 같은 형태로 스레드를 생성하는 방법입니다.
스레드를 원하는 만큼 만들수는 있지만,
Core가 작업을 할당해야 하는 스레드가 늘어나는 것이므로 부하가 발생할 수 있습니다.
따라서 멀티스레드 프로그래밍에서는 권장하지 않는 방법입니다.
사용예제는 다음과 같습니다.
class Program
{
static void AddedThread(object state)
{
for(int i = 0; i < 5; i++)
Console.WriteLine("Hello Thread");
}
static void Main(string[] args)
{
Thread t = new Thread(AddedThread);
t.Name = "Added Thread";
t.IsBackground = true;
t.Start();
Console.WriteLine("Wait for Thread");
t.Join();
Console.WriteLine("Hello World");
}
}
}
main 스레드 외에 Added Thread라는 새로운 스레드를 추가합니다.
기본적으로 위와 같이 추가되는 스레드는 백그라운드에서 동작하지 않으므로
IsBackground 속성을 true로 설정하여 main 스레드가 종료되면 같이 종료시키겠습니다.
Start() 함수로 생성한 스레드를 작동시킬 수 있습니다.
그리고 Join() 함수를 이용하여 생성한 스레드의 작업이 끝날때까지 묶어둘 수 있습니다.
실행 결과는 다음과 같습니다.
Wait for Thread가 출력된 뒤에,
Added Thread의 작업이 완료된 후, main 스레드의 작업이 실행되었습니다.
다음으로는 Thread 방식보다 훨씬 더 부하가 적은 멀티스레드 프로그래밍 방식인
ThreadPool 방식을 알아보겠습니다.
2) ThreadPool
ThreadPool은 ObjectPool과 비슷한 방식입니다.
정해진 양의 Thread들을 필요한 상황에서만 만들어냅니다.
기본적으로 Thread 방식과는 다르게 임시적인 Thread 방식으로 동작하므로 부하가 훨씬 더 적습니다.
그리고 기본 설정으로 백그라운드에서 동작하게 되어있습니다.
사용예제는 다음과 같습니다.
class Program
{
static void AddedThread(object state)
{
for(int i = 0; i < 5; i++)
Console.WriteLine("Hello Thread");
}
static void Main(string[] args)
{
ThreadPool.SetMinThreads(1, 1);
ThreadPool.SetMaxThreads(5, 5);
for (int i = 0; i < 5; i++)
ThreadPool.QueueUserWorkItem((obj) => { while (true) { } });
ThreadPool.QueueUserWorkItem(AddedThread);
while (true) { }
}
}
ThreadPool 방식에서는 최소로 사용할 스레드의 개수와 최대로 사용할 스레드의 개수를 지정해야 합니다.
따라서 이를 SetMinThreads 함수와 SetMaxThreads 함수로 진행하였습니다.
ThreadPool에서 생성한 스레드에서 작업을 진행하기 위해서는 QueueUserWorkItem 함수를 사용합니다.
이때 QueueUserWorkItem 함수에 넘겨주는 함수는 반드시 인자를 object형으로 사용해야 합니다.
위의 예제에서는 ThreadPool에서 5개의 스레드를 생성하고 람다식으로 무한 반복문을 진행하고 있습니다.
최대 스레드의 개수를 5개로 지정하였으므로 AddedThread를 호출할 스레드가 없습니다.
따라서 결과는 다음과 같습니다.
그러나 위에서 4개의 스레드를 생성하도록 코드를 수정하면 결과는 다음과 같습니다.
하나 남은 스레드가 AddedThread를 잘 수행하는 것을 확인할 수 있습니다.
ThreadPool은 기존에 생성된 쓰레드가 작업을 마치고 돌아오는 경우에 새롭게 하나를 생성합니다.
즉, 작업의 양이 짧을수록 유리하고 길수록 불리하다는 단점이 있습니다.
이를 해결하기 위해 하나의 개념을 추가하여 사용할 수 있습니다.
3) Task
Task는 ThreadPool에서 처리하기에 오래 걸리는 작업을 별도의 Thread를 만들어 수행합니다.
이를 위해 TaskCreationOptions을 인자로 넣어주어 설정함으로서 작업에 따른 Task를 미리 설정할 수 있습니다.
사용예제는 다음과 같습니다.
class Program
{
static void AddedThread(object state)
{
for(int i = 0; i < 5; i++)
Console.WriteLine("Hello Thread");
}
static void Main(string[] args)
{
ThreadPool.SetMinThreads(1, 1);
ThreadPool.SetMaxThreads(5, 5);
for (int i = 0; i < 5; i++)
{
Task t = new Task(() => { while (true) { } }, TaskCreationOptions.LongRunning);
t.Start();
}
ThreadPool.QueueUserWorkItem(AddedThread);
while (true) { }
}
}
기존의 코드에서는 ThreadPool을 5개 생성하여 무한반복문을 실행하면
AddedThread를 수행할 수 없었습니다.
하지만 Task를 새로 생성하고 설정을 TaskCreationOption.LongRunning을 설정하면
오래 수행하는 작업에 대해 별도의 Thread를 생성하게 됩니다.
따라서 수행결과는 다음과 같습니다.
AddedThread 함수를 문제없이 실행한것을 확인할 수 있습니다.
만약, Task에서 TaskCreationOption.LongRunning을 생략하면 Task가 사용한다고해도
오래 수행되는 작업에 대해 별도의 스레드를 생성하지 않게 됩니다.
따라서 결과는 다음과 같습니다.
정리하자면 설계자는 멀티스레드 프로그래밍에서 직접 Thread를 생성하기 보다는
ThreadPool과 Task를 사용하여 멀티스레드 프로그래밍을 진행하는 것이 훨씬 안정적이며 안전합니다.