전체 페이지뷰

2017년 1월 19일 목요일

C# Thread, Task

멀티프로세싱과 멀티스레딩은 다른 개념입니다. 저는 지금까지 이 두개가 그저 비슷한 말이겠거니 생각해왔는데 실상은 전혀 다르다는 것을 알게 되었습니다.


Process는 컴퓨터 내에서 연속적으로 실행되고 있는 프로그램 단위를 말합니다.
Thread는 그 프로세스 안에서 실행되는 흐름의 단위를 뜻합니다.

하나의 프로세스는 따라서 한 개 이상의 스레드 작업에 의해 구성되며, 하나의 프로그램 내에서 여러개의 스레드를 동시에 작동하는 것을 멀티스레딩이라 합니다. 다시 말해 하나의 프로세스 내의  메모리를  여러개의 스레드가 공유하며 사용한다는 것을 말합니다.
멀티 프로세스는 여러개의 프로세스가 각각의 메모리를 가지고서 독립적으로 실행되는 것을 말하는 것입니다.

그러면 멀티 프로세싱을 하는 이유는 알겠는데  멀티스레딩을 하는 이유는 무엇일까요?

서버에서 큰 파일을 다운받는 프로그램이 있다고 할 때, 멀티 스레딩이 되지 않는다면 다운 받는 동안 그 프로그램은 다운받는 일만을 하게 되므로 중간에 취소를 하고 싶어도 반응이 불가합니다. 그러면 부득이 강제종료와 같은 방법을 쓸 수 밖에 없습니다.
또한 프로세스 내에 할당된 메모리와 리소스를 그대로 이용하므로 경제성이 높습니다.

다만, 구현하기가 몹시 까다롭고, 하나의 스레드에 이상이 생기면 다른 스레드들도 전부 영향을 받게 됩니다. 그리고 너무 지나치게 스레드를 많이 쓰면 context switching이 빈번해져 오히려 성능을 저하시킬 수 있습니다.

생각만 해도 공부하기 싫어지는 설명은 그만 일고 대체 어째서 그런가 하나하나 짚어가 보도록 하겠습니다.

Thread

Thread 시작하기

System.Threading.Thread 클래스를 사용합니다.
기본적으로 Thread를 사용하는 방법은
Thread 인스턴스 생성⇒ Thread.Start()로 시작 ⇒ Thread.Join() 입니다.

static void DoWork()
{
    //
}

static void Main()
{
    Thread t = new Thread(new ThreadStart(DoWork));
    t.Start();
    t.Join();
}

위에서 보시다시피 Thread t 에 그 쓰레드가 할 일을 지정해주어 인스턴스를 만듭니다.
그리고 Start()를 만나면 그 쓰레드가 작동되고, 수행할 작업을 마치면 Join()이 실행되어 다시 쓰레드를 합쳐줍니다. 전체 프로그램이라는 메인 쓰레드가 존재하고, t로 새로운 쓰레드를 생성해 다른 일을 하는 것입니다.

예제를 보겠습니다.
using System;
using System.Threading;
public class Worker
{
    public void DoWork()
    {
        while (!_shouldStop)
        {
            Console.WriteLine("worker thread: working...");
        }
        Console.WriteLine("worker thread: terminating gracefully.");
    }
    public void RequestStop()
    {
        _shouldStop = true;
    }
    // Volatile is used as hint to the compiler that this data
    // member will be accessed by multiple threads.
    private volatile bool _shouldStop;
}
public class WorkerThreadExample
{
    static void Main()
    {
        // thread 객체를 생성. 시작은 아님
        Worker workerObject = new Worker();
        Thread workerThread = new Thread(workerObject.DoWork);
        // worker thread 시작
        workerThread.Start();
        Console.WriteLine("main thread: Starting worker thread...");
        // worker thread가 활성화 될 때까지 루프를 돌림.
        while (!workerThread.IsAlive) ;
        // Put the main thread to sleep for 1 millisecond to
        // allow the worker thread to do some work:
        Thread.Sleep(1);
        //  worker thread가 멈추도록 요청
        workerObject.RequestStop();
        
        workerThread.Join();
        Console.WriteLine("main thread: Worker thread has terminated.");
    }
}
cs

결과)
main thread: Starting worker thread...
worker thread: working...
worker thread: working...

...

worker thread: working...
worker thread: working...
worker thread: terminating gracefully.
main thread: Worker thread has terminated.

Thread의 좀더 깊은 내용에 대해서는 MSDN 링크를 거는 것으로 대신하겠습니다.
아무래도 과거의 기술인데다가 복잡하여 좀더 최근 기술인 Task를 자세히 살펴보는 것으로 대신하려 합니다. 사실 엄밀히 말해 두 가지가 같은 기능을 한다고 생각하긴 어렵지만 아무래도 제가 Thread를 사용할 일이 많을 것 같지는 않네요.

Task, Task<TResult>, Parallel

하드웨어의 개발방향이 CPU 클럭스피드 높이기에서 코어 수를 높이는 쪽으로 살짝 방향선회 되면서 병렬처리, 비동기 처리 기법들은 점점 더 중요해졌습니다.

병렬처리, 비동기 처리를 라면을 끓이는 예를 들어 설명해 보겠습니다.

먼저 물을 불에 올려놓고, 라면 봉지를 열고, 상을 차리다가 물이 끓으면 라면과 스프를 넣고 익을 때까지 상을 마저 차리고, 다 끓으면 라면을 상에 갖다놓는...이런 과정을 비동기 처리에 비유할 수 있을 것 같습니다. 물이 끓을 때까지 아무 것도 하지 않다가 다 끓어서야 다음 단계를 진행하는 게 아니기 때문입니다.

이번엔 여러명이서 한명은 라면을 끓이고, 한명은 상을 차리고, 한명은 쓰레기를 정리하고...등을 하는 것을 병렬 처리라 합니다. 

이런 과정을 돕는 기술이 System.Threading.Tasks 네임스페이스와 나중에 살펴 볼 async await에 들어 있습니다.

System.Threading.Tasks.Task 클래스

Task는 비동기 처리를 쉽게 해줄 수 있도록 도와줍니다. asunc, await로 구현할 수도 있지만 일단은 Task를 먼저 살펴봅시다.

Task는 인스턴스 생성 시 Action 델리게이트를 넘겨받습니다.

Action action = () =>
{
    Thread.Sleep(1000);
    Console.WriteLine("Async. printing");
};

Task myTask = new Task(action);
myTask.Start();   //비동기 호출

Console.WriteLine("Sync. printing");

myTask.Wait();

과 같은 방식으로 쓰입니다.
무명함수 형식의 Action 델리게이트 객체를 생성하고,
action을 실행하는 Task 인스턴스를 생성합니다. Start() 메소드로 비동기 실행하면, 1초를 기다리게 되고, 그 동안 "Sync. printing"을 출력하고 비동기인 myTask가 끝날때까지 기다립니다.

좀 더 일반적인 방식은 Start()가 아닌 Run()을 사용하는 것입니다.

var myTask = Task.Run( () =>
    {
        Thread.Sleep(1000);
        Console.WriteLine("Async.  printing");
    }
);

Console.WriteLine("Sync. printing");

myTask.Wait();

예제를 작성해 봅니다.

using System;
using System.Threading;
using System.Threading.Tasks;
public class Example
{
    static void Main()
    {
        // Wait for all tasks to complete.
        Task[] tasks = new Task[10];
        for (int i = 0; i < 10; i++)
        {
            tasks[i] = Task.Run(() => Thread.Sleep(1000));
        }
                
        try
        {
            Task.WaitAll(tasks);
        }
        catch (AggregateException ae)
        {
            Console.WriteLine("One or more exceptions occurred: ");
            foreach (var ex in ae.Flatten().InnerExceptions)
                Console.WriteLine("   {0}", ex.Message);
        }
        
        Console.WriteLine("Status of completed tasks:");
        foreach (var t in tasks)
            Console.WriteLine("   Task #{0}: {1}", t.Id, t.Status);
    }
}
cs

결과)
Status of completed tasks:
   Task #2: RanToCompletion
   Task #1: RanToCompletion
   Task #3: RanToCompletion
   Task #4: RanToCompletion
   Task #5: RanToCompletion
   Task #7: RanToCompletion
   Task #6: RanToCompletion
   Task #9: RanToCompletion
   Task #8: RanToCompletion
   Task #10: RanToCompletion

Task<TResult>

반환값이 있는 Task의 비동기 실행결과를 쉽사리 취합할수 있도록 도와주는 클래스입니다.
말하자면 Task의 Func 버전(Action이 아닌)이라고 할 수 있겠습니다.

var t = Task<int>.Run( () => {
                                  int max = 1000000;
                                  int ctr = 0;
                                  for (ctr = 0; ctr <= max; ctr++) {
                                     if (ctr == max / 2 && DateTime.Now.Hour <= 12) {
                                        ctr++;
                                        break;
                                     }
                                  }
                                  return ctr;
                               } );
Console.WriteLine("Finished {0:N0} iterations.", t.Result);

사용방식은 Task와 비슷합니다.



Parallel class

이 클래스를 이용하면 For(), Foreach()의 메소드를 사용해서 Task<TResult>에서 직접 구현했던 병렬처리를 더 쉽게 해줍니다.

// named method 사용
Parallel.For(0, N, Method2);

// 무명 메소드 사용
Parallel.For(0, N, delegate(int i)
{
    // Do Work.
});

// 람다식 이용
Parallel.For(0, N, i =>
{
    // Do Work.
});

의 형태로 사용합니다. Parallel을 이용해서 소수를 찾는 뇌자극 C# 5.0의 예제를 한번 따라해 보겠습니다.

using System;
using System.Threading;
using System.Collections.Generic;
using System.Threading.Tasks;
 
namespace ParallelExample
{
    class Program
    {
        static bool IsPrime(long number)
        {
            if (number < 2)
                return false;
            if (number % 2 == 0 && number != 2)
                return false;
            for(long i = 2; i < number; i++)
            {
                if (number % i == 0)
                    return false;
            }
            return true;
        }
        static void Main()
        {
            long from = Convert.ToInt64(Console.ReadLine());
            long to = Convert.ToInt64(Console.ReadLine());
 
            Console.WriteLine("Press enter to start...");
            Console.ReadLine();
            Console.WriteLine("시작...");
 
            DateTime start = DateTime.Now;
            List<long> total = new List<long>();
 
            Parallel.For(from, to, (long i) =>
             {
                 if (IsPrime(i))
                     total.Add(i);
             });
            DateTime end = DateTime.Now;
            TimeSpan ellapsed = end - start;
 
            Console.WriteLine("{0}과 {1} 사이의 소수 갯수는 {2}개", from, to, total.Count);
            Console.WriteLine("계산 소요 시간 : {0}", ellapsed);        
        }
    }
   
}
cs

결과)
0
100000
Press enter to start...

시작...
0과 100000 사이의 소수 갯수는 9588개
계산 소요 시간 : 00:00:00.8437459

async,  await

C# 5.0부터 도입되었습니다.
async 한정자로 수식된 메소드, 람다식, 무명 메소드를 비동기로 지정합니다.
따라서 코드 내에서 이 부분을 만나면 결과를 기다리지 않고 다음으로 이동하여 작업을 수행합니다.

아무 메소드나 가능한 것은 아니며 반환형식이 Task, Task<TResult>, void 셋 중의 하나여야만 합니다.

void의 경우는 await 구문이 없어도 비동기로 실행되며,
Task, Task<TResult>의 경우는 await 연산자가 있어야 비동기롤 실행됩니다.

예제를 들어서 살펴 보겠습니다.

using System;
using System.Threading.Tasks;
 
namespace AsyncAwait
{
    class Program
    {
        async static private void AsyncMethod(int count)
        {
            Console.WriteLine("1...");
            await Task.Run(async () =>
            {
                for (int i = 1; i <= count; i++)
                {
                    Console.WriteLine("{0}/{1}", i, count);
                    await Task.Delay(100);
                }
            });
 
            Console.WriteLine("2...");
        }      
        
        static void Caller()
        {
            Console.WriteLine("3...");
 
            AsyncMethod(3);
 
            Console.WriteLine("4...");
        }  
        static void Main()
        {
            Caller();
            Console.ReadLine(); // 콘솔 종료 방지
        }
    }   
}
cs

댓글 1개: