In to the C# 2014. 1. 26. 22:41

[C#] Task의 작업완료

Task는 기본적으로 ThreadPool을 사용하여 비동기 작업을 쉽게 구현할 수 있도록 도와 줍니다.

작업 후 동기화는 방법도 여러가지가 있습니다. 먼저 예제가 있습니다.

 

Line 1 ~ 2까지는 Action을 정의 한 후 Task로 시작 하였습니다.

Line 4는 Task의 작업을 기다리고 있습니다. Task의 작업이 오래 걸리면 중간의 작업이 완료될 때 까지 기다릴 수 있습니다.

Line 5는 조금 색다른 기능입니다. Task의 작업이 끝나면 t1의 작업이 끝나면 파라미터로 넘긴 익명메소드가 실행이 될 수 있도록 합니다. 이런 작업은 미리 후반 작업을 설정함으로써 Task 작업이 언제 종료될지 기다리고 관리할 필요가 없어집니다.

예제에는 _t1은 t1의 작업이 끝나면 파라미터 익명 메소드에 자기 자신을 파라미터로 넘깁니다.

Line 6은 Line 5와 똑같으며, 단시 리턴값이 있는 것입니다. Line 7은 return 값을 화면에 출력하는 것입니다.

 

Line 7에서 retTask.Result를 사용했는데 Result를 호출하면 retTask의 작업이 완료되기 까지 기다립니다. 중간에 Wait 메소드를 호출할 필요가 없는 거죠. 어차피 Result를 호출한다는 것은 작업이 완료되기 까지 기다리겠다는 뜻이니까요.

 

여기까지 Task의 동기화에 대해서 알아봤습니다.

 

'In to the C#' 카테고리의 다른 글

C# 6.0 New Features  (2) 2015.01.29
[C#] REST API 만들기  (0) 2014.02.13
[C#] Action, Func 그리고 Task  (0) 2014.01.26
[C#] Attribute : 속성  (0) 2014.01.24
Lazy Initialization  (0) 2014.01.20
In to the C# 2014. 1. 26. 22:40

[C#] Action, Func 그리고 Task

C#의 대리자(delegate)와 Task에 대해서 알아보겠습니다. 대리자와 Task는 항상 따라다니는 기술이며, 수많은 오픈소스도 이 두개의 개념을 적극적으로 사용하여, 비동기 로직을 구현하고 있습니다. Action과 Func은 대리자로 구현되어져 있습니다. Action이 어떻게 구현 되었는지 보겠습니다.

 

정말 별거 없습니다.

잠시만!! Action과 Func의 차이점에 대해서 얘기 하자면, Action은 파라미터만 있는 대리자 역활을 합니다. 즉, 리턴 할 수 없는 대리자 입니다. 반면에 Func은 파라미터 및 리턴 모두 할 수 있는 대리자 입니다. 이 둘의 차이점은 이것 말고는 없습니다.

 

Action은 이런 식으로 16개의 파라미터를 갖을 수 있으며, Func은 16개의 파라미터와 1개의 리턴을 갖을 수 있습니다.

Action과 Func은 굳이 정의할 필요가 없을 듯 보이지만 많은 라이브러리에서 이런 공통적인 대리자를 사용함으로써 사용자의 입장에서 보면 어려움 없이 사용할 수 있도록 하였습니다.

 

간단히 Action 정의 및 사용법에 대해 보겠습니다.

 

Line1을 보면 파라미터 없는 Action을 사용한 것입니다. action이라는 대리자에 익명함수를 정의 한 것입니다.

Line2는 string 파라미터를 갖는 Action입니다. Line3은 string과 int형 파라미터를 갖는 Action 입니다.

Line 5 ~ 7은 함수 사용하듯이 사용합니다.

 

이번엔 Func 입니다.

 

Func은 리턴 값이 꼭 있어야 합니다. 리턴 타입은 가장 마지막에 적어주면 됩니다.

 

이렇듯이 Action과 Func은 함수를 객체처럼 사용할 수 있습니다. 지금까지 Action과 Func을 본 이유는 Task를 설명하기 위해서 입니다.

Task는 C#에서 비동기 작업을 작성하기 위한 코드를 단순화 시켜줍니다. Task는 닷넷프레임워크에서 제공하는 ThreadPool에서 작동합니다. 아쉽지만 Task를 사용할 때는 순서나 시작 시점을 사용자가 지정할 수 없습니다. 즉, Task는 어떠한 비동기 작업을 수행한다라고만 지정해 줄 뿐 수행에 대한 컨트롤을 하기 어렵습니다. 그런데 Task를 왜 쓰냐면 아래와 같습니다.

 

1. 이미 만들어진 Thread에서 작동하기 때문에 Thread를 생성할 필요가 없습니다. Thread생성 비용을 줄일 수 있습니다.

2. 프로그래밍의 복잡도가 낮아집니다. 익명함수를 만들어 Task 실행만 해주면 됩니다.

3. 프로그래밍 복잡도가 낮아지므로써 병행 작업이 가능한 코딩이 쉽습니다.

4. ThreadPool의 모든 Thread가 작업중일 때는 추가 Thread가 생성이 됩니다. 이는 언덕등반오르기 알고리즘을 사용합니다.

 

마이크로소프트에서는 병행프로그래밍을 작성할 때는 가능한 Task를 사용하길 권하고 있습니다. 빠르고 안전하기 때문입니다. 그럼 언제 Thread를 사용하느냐하면,

 

1. 단독 쓰레드에서 안정적으로 지속적인 백그라운드 작업이 필요할 경우 Thread를 생성하여 작업 합니다.

 

굳이 시작시점과 종료시점을 정해야 할 작업이 아니라면 Task를 사용하는 것들 권하는 것입니다. 편하고, 빠르니까요.

 

이 Task는 생성될 때 생성자 파라미터에 Action, Action<object>, Func<TResult>, Func<object, TResult> 형식을 넘길 수 있습니다. 아래와 같이 말이죠.

 

이렇게 Task는 대리자를 통한 익명함수를 넘겨주면서 작업을 비동기 방법으로 수행 할 수 있도록 해줍니다.

Task는 굳이 생성자로 만들고 Start를 해줄 필요는 없습니다. Task는 Factory 메소드가 있어 익명함수를 만들어 넘겨주면 바로 실행 할 수 있도록 해주는 기능도 있습니다.

 

이렇게 하면 Task 생성 및 Start 메소드 호출을 할 필요는 없습니다. 여기까지 Action과 Func 그리고 이들을 사용하는 Task와 Task의 시작에 대해서 알아봤습니다. 다음 포스트에는 Task를 사용하는 방법에 대해서 간단히 알아보겠습니다.

 

'In to the C#' 카테고리의 다른 글

[C#] REST API 만들기  (0) 2014.02.13
[C#] Task의 작업완료  (2) 2014.01.26
[C#] Attribute : 속성  (0) 2014.01.24
Lazy Initialization  (0) 2014.01.20
BookSleeve - Pipelined .NET bindings for redis  (0) 2014.01.17
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를 생성해서 사용하여야 합니다.


끝~