TaskCompletionSource<T> Async


참조 URL
  1. TaskCompletionSource<TResult> 클래스
  2. C#5, ASP.NET MVC 4, and asynchronous Web applications

 

 이 포스트에 있는 내용이 언제나 확실한 정답은 아닙니다. 진실이라고 생각해 왔던 전제가 시간의 지남에 따라 들어나지 않았던 다른 이면 때문에 좋은 방향으로 이끌어 낼 수 있는 역할로 변환 되는게 역사적으로도 많은 증명 있었습니다. 그렇지만 저는 현재 상황에서 최선의 답을 찾고자 노력하였으며 이 글을 읽는 다른 분들에게 다음 길을 갈 수 있도록 도와주는 디딤돌이 되고자 노력하고자 포스팅을 통해 공유하고자 하는 것입니다. 그리고 프로그래머라는 타이틀을 달고 살아야 한다면 "왜"라는 의문을 항상 가지고 다니면서 자신의 위치에 안주하지 않고 항상 노력하는 모습으로 살아 가고자 합니다. 언제든 지적이나 오류가 있으면 피드백 부탁 드리겠습니다.

ing™       



 Task<T> 객체를 이용해서 비 동기로 작업을 손 쉽게 수행하도록 프로그램을 개발할 수 있다. 그렇지만 TaskCompletionSource<T>를 사용하면 명시적으로 결과값 리턴을 통제할 수 있게 할 수 있다. 우선 프로그램 화면을 보도록 하자.


[그림1] WPF 테스트 화면


 테스트를 해볼 시나리오는 "대기 시작" 버튼을 누르고 대기 종료 버튼을 누르면 그 사이의 대기 시간을 TextBlock 화면에 표시해주는 시나리오를 가지고 프로그램으로 개발해 보도록 하겠다.


[코드1] WPF에서 '대기 시작' 버튼 코드




[코드2] WPF에서 "대기 종료" 버튼 코드



 이번 포스트의 소스 파일을 첨부할테니 직접 실행해 볼 수 있을 것이다.


TaskCompletionSourceWPF.zip




소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다. 실 개발에서도 적용할 수 있도록 간단하면서도 현실적인 예제 프로그램을 통해 각 소스를 만들고 이해 시키고자 하였으며 실무에 필요한 개발요구 사항들을 해결 하는데 도움이 되고자 노력하였습니다. 그리고 소스와 같이 있는 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       



C# 낮은 수준의 동기화 SpinLock

http://msdn.microsoft.com/ko-kr/library/dd997366.aspx


 우리가 흔히 사용하는 Lock 객체와 같은 일을 하는 클래스가 .Net Framework 4에서 새로 나왔다. SpinLock 객체가 하는 역할은 Lock과 같은 역할을 하지만 새로 나온 이유가 있을 것이다. MSDN에서는 아래와 같이 설명하고 있다.


대기 시간이 짧을 것으로 예상되고 경합이 적으면 다른 종류의 잠금보다 SpinLock을 수행하는 것이 효과적입니다. 그러나 SpinLock은 System.Threading.Monitor 메서드 또는 Interlocked 메서드로 인해 프로그램 성능이 크게 떨어진다는 점이 프로파일링을 통해 확인될 때만 사용하는 것이 좋습니다. 
http://msdn.microsoft.com/ko-kr/library/dd997366.aspx

[표1] SpinLock MSDN 설명 발췌


 위 글에서 Monitor은 Lock이 컴파일이 되면 내부적으로 Monitor로 대체되기 때문에 Lock을 따로 언급하지 않은것으로 보인다. 그렇다면 위 이야기대로라면 SpinLock이 모든 lock 상황에서 성능 우위를 지원하는 것은 아님을 알수 있으니 개발하는 입장에서는 일일이 확인해 보고 적용을 하라는 뜻이다. ^^;;

그래도 SpinLock를 사용해야 하는 상황이 특출나게 성능을 요하는 작업에 대해서만 고려하면 되기 때문에 그나마 다행이라 할 수 있겠다.


 이제 본격적으로 검토를 해 보자. SpinLock와 Lock(Monitor)을 사용해서 성능 비교를 해 보도록 하자.

// 잠김 횟수
const int N = 100000;
static Queue<LockDataObject> _spinLockQueue = new Queue<LockDataObject>();
static Queue<LockDataObject> _lockQueue = new Queue<LockDataObject>();
 
// 잠김에 사용할 객체
static object _lock = new Object();
// 잠김에 사용할 SpinLock 객체
static SpinLock _spinlock = new SpinLock();
 
// 수행에 필요한 데이터 객체
class LockDataObject
{
    public string Name { getset; }
    public double Number { getset; }
}
 
/// <summary>
/// Lock 객체로 잠금 상태에서 Queue에 넣기
/// </summary>
/// <param name="d"></param>
/// <param name="i"></param>
private static void UpdateWithSpinLock(LockDataObject dint i)
{
    // false로 세팅하고 spinLock 객채에 Enter해야 한다.
    bool lockTaken = false;
    try
    {
        _spinlock.Enter(ref lockTaken);
 
        // working
 
        // CPU 사이클 동안 대기
        Thread.SpinWait(500);
 
        // Queue에 넣는건 제외
        // 메모리에 넣고 가비지 컬렉터의 영향을 덜 받기 위해 주석 처리 함.
        //_spinLockQueue.Enqueue(d);
    }
    catch (LockRecursionException ex)
    {
        Console.WriteLine("{0}, {1}"Thread.CurrentThread.ManagedThreadIdex.Message);
    }
    finally
    {
        // 잠김 풀기
        if (lockTaken)
        {
            _spinlock.Exit(false);
        }
    }
}
 
/// <summary>
/// SpinLock 객체로 잠금 병렬 수행
/// </summary>
private static void UseSpinLock()
{
    Stopwatch sw = Stopwatch.StartNew();
 
    // 병렬 실행
    Parallel.Invoke(
            () =>
            {
                for (int i = 0i < Ni++)
                {
                    UpdateWithSpinLock(new LockDataObject() { Name = i.ToString(), Number = i }, i);
                }
            },
            () =>
            {
                for (int i = 0i < Ni++)
                {
                    UpdateWithSpinLock(new LockDataObject() { Name = i.ToString(), Number = i }, i);
                }
            }
        );
 
    sw.Stop();
    Console.WriteLine("elapsed ms with spinlock: {0}"sw.ElapsedMilliseconds);
}
 
/// <summary>
/// Lock 객체로 잠금 상태에서 Queue에 넣기
/// </summary>
/// <param name="d"></param> 
/// <param name="i"></param>
static void UpdateWithLock(LockDataObject dint i)
{
    lock (_lock)
    {
        // working
 
        // CPU 사이클 동안 대기
        Thread.SpinWait(500);
        // Queue에 넣는건 제외
        // 메모리에 넣고 가비지 컬렉터의 영향을 덜 받기 위해 주석 처리 함.
        //_lockQueue.Enqueue(d);
    }
}
 
/// <summary>
/// Lock 객채로 잠금을 사용해서 병렬 수행
/// </summary>
private static void UseLock()
{
    Stopwatch sw = Stopwatch.StartNew();
 
    // 병렬 실행
    Parallel.Invoke(
            () =>
            {
                for (int i = 0i < Ni++)
                {
                    UpdateWithLock(new LockDataObject() { Name = i.ToString(), Number = i }, i);
                }
            },
            () =>
            {
                for (int i = 0i < Ni++)
                {
                    UpdateWithLock(new LockDataObject() { Name = i.ToString(), Number = i }, i);
                }
            }
        );
 
    sw.Stop();
    Console.WriteLine("elapsed ms with lock: {0}"sw.ElapsedMilliseconds);
}

[코드1] SpinLock와 Lock 성능 비교


 소스 코드상의 주석에서도 언급 했듯이 어떤 케이스를 먼저 실행 하느냐에 따라서 결과 값이 상이하게 나오고 있으며 SpinLock를 사용하여 테스트를 하면 프로그램 시작후 가장 처음에 실행되었을때 가장 안 좋은 결과치를 지속적으로 보여주고 있었다. 물론 지금의 성능 비교를 하는 케이스가 SpinLock를 만든 목적에 부합되지 않을 수도 있기 때문에 그런 현상이 발생할 수도 있을 것이다. 두 케이스의 비교를 통해 결론을 내리자면 위 상황에서는 대체적으로 Lock 객체를 사용해서 잠금 발생을 하는것이 성능 우위를 나타내고 있었다. 그렇지만 다른 케이스에서는 어떻게 결과가 나올 지 알수 없으므로 Lock 객체만으로도 성능이 제대로 나오지 않으면 SpinLock를 테스트 해보기를 권한다. 아래 "그림1"에서 결과 화면을 보도록 하자.


[그림1] SpinLock vs Lock 결과 화면

(테스트 환경 : i5 모바일 CPU, 2Core, 하이퍼스레딩, 8GB )



 그리고 Parallel.Invoke에 대해서 부연 설명을 드리자면 Task를 두개 생성하면 실행하는 구문이라고 생각하면 쉽게 이해할 수 있을 것이다. Parallel.Invoke(Task(action), Task(action))와 같은 형식이고 각 타스크가 쓰레드에서 타스크 형식으로 작업이 수행되는 것이다. 두개를 한번에 실행 하기에 잠김 상태가 발생하도록 한 것이다. 그리고 SpinLock()는 해당 CPU 주기만큼 대기 하고 실행하도록 해주는 역할을 한다. Thread.Sleep()는 시간이 비교 대상이라면 SpinLock는 CPU 싸이클이 비교대상인 것이다.


소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다.. 실제 개발에서도 필요한 소스는 단순히 Copy & Paste 만으로도 사용할 수 있습니다. 그리고 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       


C# TaskFactory and TaskScheduler

http://msdn.microsoft.com/ko-kr/library/dd321418.aspx

http://code.msdn.microsoft.com/ParExtSamples

http://msdn.microsoft.com/ko-kr/library/dd997402.aspx

http://channel9.msdn.com/Events/TechDays/Techdays-2012-the-Netherlands/2287


 이전 포스트에서 MyTaskScheduler을 직접 만들어 보았다. 그러나 동시성 수준이 1이라서 비 동기 효율이 제대로 나오지 않는 구조적인 문제가 있었다. 실제로 테스트를 해보면 Default Scheduler 보다 훨씬 안 좋은 성능을 보여주고 있다. 그리하여 이번에는 동시성 수준을 마음대로 컨트롤 할 수 있는 LimitedConcurrencyLevelTaskScheduler을 만들어 보도록 하겠다.

/// <summary> /// TaskScheduler을 상속 받아 구현 한다. /// Provides a task scheduler that ensures a maximum concurrency level while /// running on top of the ThreadPool. /// </summary> public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler {     /// <summary>     /// 현재 쓰레드가 작업 리스트를 처리하고 있는지 여부 판단     /// </summary>     /// <remarks>     /// ThreadStaticAttribute 로 표시된 static 필드는 스레드 간에 공유되지 않습니다.     /// 각 실행 스레드에는 필드에 대한 별도의 인스턴스가 있으며 해당 필드에 대한 값을 독립적으로 설정하고 가져옵니다.     /// 필드를 서로 다른 스레드에서 액세스하면 해당 필드에는 다른 값이 들어가게 됩니다.     /// </remarks>     [ThreadStatic]     private static bool _currentThreadIsProcessingItems;     /// <summary>실행될 타스크 리스트</summary>     private readonly LinkedList<System.Threading.Tasks.Task> _tasks = new LinkedList<System.Threading.Tasks.Task>(); // protected by lock(_tasks)     /// <summary>현재 스케줄러에서 최대로 허용된 동시성 제어 수준</summary>     private readonly int _maxDegreeOfParallelism;     /// <summary>실제로 수행되고 있는 동시성 수준 숫자</summary>     private int _delegatesQueuedOrRunning = 0// protected by lock(_tasks)     /// <summary>     /// 인스턴스 초기화 진행     /// </summary>     /// <param name="maxDegreeOfParallelism">최대 동시성 수준 허용 갯수</param>     public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism)     {         if (maxDegreeOfParallelism < 1throw new ArgumentOutOfRangeException("maxDegreeOfParallelism");         _maxDegreeOfParallelism = maxDegreeOfParallelism;     }     /// <summary>연결된 쓰레드에서 동기 Task 제공해 준다</summary>     /// <param name="task">큐에 대기할 Task입니다.</param>     protected sealed override void QueueTask(System.Threading.Tasks.Task task)     {         // 처리할 작업 목록에 추가.         // tasks가 처리중이거나 준비가 되지 않았을 때 대기 함         lock (_tasks)         {             _tasks.AddLast(task);             // MaximumConcurrencyLevel의 숫자보다 작을 때만 실행             // 동시성 수준을 제어하는 Scheduler 클래스 이므로 이곳에서 동시성 수준을 체크한다.             if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism)             {                 ++_delegatesQueuedOrRunning;                 NotifyThreadPoolOfPendingWork();             }         }     }     /// <summary>     /// 작업이 스케줄러에 대한 실행 할 필요가있는 스레드를 알려줍니다.     /// </summary>     private void NotifyThreadPoolOfPendingWork()     {         // ThreadPool에서 실행이 되도록 한다.         // 실행 주기는 ThreadPool이 CPU 환경에 맞게 동시 실행을 컨트롤 한다.         // http://msdn.microsoft.com/ko-kr/library/system.threading.threadpool_methods(v=vs.110).aspx         ThreadPool.UnsafeQueueUserWorkItem(_ =>         {

        // 현재 스레드가 작업 항목을 활성화 하도록 함
        // 스레드에 작업 활성화를 할 수 있도록 한다.
        // ThreadStatic으로 선언되었기에 쓰레드마다 별도의 값으로 접근 한다.
        _currentThreadIsProcessingItems = true;
        try
        {
            // 대기열에 사용 가능한 모든 항목을 처리합니다.
            while (true)
            {
                System.Threading.Tasks.Task item;
                lock (_tasks)
                {
                    // 처리 할 항목이 더 있을 경우,
                    // 처리가 완료되면 루프를 나간다.
                    if (_tasks.Count == 0)
                    {
                        --_delegatesQueuedOrRunning;
                        break;
                    }
 
                    // 큐에서 다음 항목을 가져 오기.
                    item = _tasks.First.Value;
                    _tasks.RemoveFirst();
                }
 
                // 큐에서 찾아낸 작업을 실행
                base.TryExecuteTask(item);
            }
        }
        // 현재 스레드에서 처리 항목을 완료
        finally { _currentThreadIsProcessingItems = false; }
                    }, null);     }     /// <summary>연결된 쓰레드에서 동기 Task 제공해 준다</summary>     /// <param name="task">실행할 타스크</param>     /// <param name="taskWasPreviouslyQueued">작업이 이전에 큐에 대기되었는지 여부를 나타내는 부울입니다.이 매개 변수가 True이면 작업이 이전에 큐에 대기된 것일 수 있습니다. False이면 작업이 큐에 대기되지 않은 것입니다. 작업을 큐에 대기하지 않고 인라인으로 실행하려면 이 호출을 수행합니다.</param>     /// <returns>작업이 인라인으로 실행되었는지 여부를 나타내는 부울 값입니다. 성공적인 실행 시 True, 그 이외에 false</returns> /// <remarks>재진입으로 인한 오류를 방지하기 위해 작업 인라이닝은 관련된 스레드의 로컬 큐에서 대기 중인 대상이 있는 경우에만 발생합니다.</remarks>     protected sealed override bool TryExecuteTaskInline(System.Threading.Tasks.Task taskbool taskWasPreviouslyQueued)     {         //쓰레드에서 처리가 되고 있으면 별도 실해을 지정하지 않는다.         //중복 실행이 되지 않도록 해야 한다.         if (!_currentThreadIsProcessingItemsreturn false;         // 작업이 이전에 큐에 대기된 것이면 제거         if (taskWasPreviouslyQueuedTryDequeue(task);         // 한번더 실행을 시도 한다.         return base.TryExecuteTask(task);     }     /// <summary>이전에 이 스케줄러의 큐에 대기된 Task를 큐에서 제거하려고 합니다</summary>     /// <param name="task">큐에서 제거할 Task입니다.</param>     /// <returns>task 인수가 큐에서 제거되었는지 여부를 나타내는 부울입니다.</returns>     protected sealed override bool TryDequeue(System.Threading.Tasks.Task task)     {         lock (_tasksreturn _tasks.Remove(task);     }     /// <summary>이 TaskScheduler가 지원할 수 있는 최대 동시성 수준을 나타냅니다.</summary>     public sealed override int MaximumConcurrencyLevel { get { return _maxDegreeOfParallelism; } }     /// <summary>디버거를 지원하기 위해 현재 스케줄러의 큐에 대기되어 실행을 기다리고 있는 Task 인스턴스의 열거 가능한 형식을 생성합니다.</summary>     /// <returns>디버거가 현재 이 스케줄러의 큐에 대기된 작업을 트래버스할 수 있도록 허용하는 열거 가능한 형식입니다.</returns>     protected sealed override IEnumerable<System.Threading.Tasks.Task> GetScheduledTasks()     {         bool lockTaken = false;         try         {             Monitor.TryEnter(_tasksref lockTaken);             if (lockTakenreturn _tasks.ToArray();             else throw new NotSupportedException();         }         finally         {             if (lockTakenMonitor.Exit(_tasks);         }     } }

[코드1] LimitedConcurrencyLevelTaskScheduler 전체 코드


 위와 같이 동시성 수준을 제어 할 수 있는 스케줄러를 만들어 보았다. 처음 인스턴스를 시킬때 넣는 동시성 수준 갯수를 세팅하면 한번에 실행되는 타스크의 갯수를 제어할 수가 있다. 전체적인 흐름은 QueueTask를 통해 넘어온 Task를 곧바로 실행하지 않고 Queue에 넣어 둔다. 동시성 수준을 통과한 상태에서 NotifyThreadPoolOfPendingWork에서 ThreadPool에서 각각의 Task를 실행이 활성화 되도록 한다. 그렇다면 이제 한번 실행하여 결과를 보도록 하자.


/// <summary>
/// LimitedConcurrencyLevelTaskScheduler로 테스트
/// </summary>
public void LimitedConcurrencyLevelTaskScheduler_TestMethod()
{
    // 시간을 재기 위해서 사용
    Stopwatch sw = new Stopwatch();
    sw.Start();
 
    var limitedScheduler = new LimitedConcurrencyLevelTaskScheduler(5);
 
    // 커스터마이징 된 LimitedConcurrencyLevelTaskScheduler을 이용해 TaskFactory를 생성 하도록 한다.
    var factory = new TaskFactory(limitedScheduler);
    var tasks = new List<System.Threading.Tasks.Task>();
 
    for (int j = 1j <= 20000j++)
    {
        var task = factory.StartNew(() =>
        {
            for (int i = 0i < 5i++)
            {
                var a = Thread.CurrentThread.ManagedThreadId;
                Console.WriteLine("{0} on thread {1}"iThread.CurrentThread.ManagedThreadId);
            }
        });
 
        tasks.Add(task);
    }
 
    // 모두 완료가 될 때까지 대기
    System.Threading.Tasks.Task.WaitAll(tasks.ToArray());
 
    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds + "ms");
}

[코드2] LimitedConcurrencyLevelTaskScheduler 테스트 코드



[그림1] LimitedConcurrencyLeveTaskScheduler 실행 결과 화면


 "그림1"에서와 같이 여러 쓰레드 ID에서 각각의 Task가 수행이 된 것을 확인할 수 있다. 그렇지만 이 작업은 Default Scheduler 보다 작업 시간이 길게 걸린다. 뭔가가 문제가 있는 것일까? 예상 기대치 보다 좋지 않다란 생각을 하게 되었다. 그래서 Default Scheduler의 기본 MaximumConcurrencyLevel을 확인해보니 2147483647로 확인이 되었다. 내가 세팅한 값보다 엄청 많은 동시성 수준이다. 그리고 기본적으로 ThreadPool을 통해서 수행하다 보니 CPU Core의 절대적인 숫자에 제한을 받는다. ( ThreadPool Click new ) 그러므로 아무리 동시성 수준을 높여도 Core 갯수 이상은 동시 실행이 되지 않는다. 이 요건은 처음 포스트 당시 언급 했던 대기 시간이 많은 수행에 대해서 특별한 Scheduler를 만들려고 하는 계획과는 차이가 있게 되었다.  그래서 ThreadPool 대신이 Thread를 통해서 실행이 되도록 하였으며 프로퍼티를 통해 Thread와 ThreadPool을 선택하여 수행 할 수 있도록 수정 하게 되었다. "코드3"를 확인해 보자


/// <summary>
/// TaskScheduler을 상속 받아 구현 한다.
/// Provides a task scheduler that ensures a maximum concurrency level while
/// running on top of the ThreadPool.
/// </summary>
public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
{
    /// <summary>
    /// 현재 쓰레드가 작업 리스트를 처리하고 있는지 여부 판단
    /// </summary>
    /// <remarks>
    /// ThreadStaticAttribute 로 표시된 static 필드는 스레드 간에 공유되지 않습니다.
    /// 각 실행 스레드에는 필드에 대한 별도의 인스턴스가 있으며 해당 필드에 대한 값을 독립적으로 설정하고 가져옵니다.
    /// 필드를 서로 다른 스레드에서 액세스하면 해당 필드에는 다른 값이 들어가게 됩니다.
    /// </remarks>
    [ThreadStatic]
    private static bool _currentThreadIsProcessingItems;
    /// <summary>실행될 타스크 리스트</summary>
    private readonly LinkedList<System.Threading.Tasks.Task> _tasks = new LinkedList<System.Threading.Tasks.Task>(); // protected by lock(_tasks)
    /// <summary>현재 스케줄러에서 최대로 허용된 동시성 제어 수준</summary>
    private readonly int _maxDegreeOfParallelism;
    /// <summary>실제로 수행되고 있는 동시성 수준 숫자</summary>
    private int _delegatesQueuedOrRunning = 0// protected by lock(_tasks)
 
    /// <summary>
    /// Task 활성화 실행 타입을 설정 (ThreadPool이 기본 값)
    /// </summary>
    private LimitedConcurrencyLevelTaskExecuteType ExecuteType = LimitedConcurrencyLevelTaskExecuteType.ThreadPool;
 
    /// <summary>
    /// 인스턴스 초기화 진행
    /// </summary>
    /// <param name="maxDegreeOfParallelism">최대 동시성 수준 허용 갯수</param>
    public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism)
    {
        if (maxDegreeOfParallelism < 1throw new ArgumentOutOfRangeException("maxDegreeOfParallelism");
        _maxDegreeOfParallelism = maxDegreeOfParallelism;
    }

    /// <summary>
    /// 인스턴스 초기화 진행
    /// </summary>
    /// <param name="maxDegreeOfParallelism">최대 동시성 수준 허용 갯수</param>
    /// <param name="ExecuteType">Task수행 활성화 타입</param>
    public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelismLimitedConcurr

ncyLevelTaskExecuteType ExecuteType)         : this(maxDegreeOfParallelism)     {         this.ExecuteType = ExecuteType;     }

    /// <summary>연결된 쓰레드에서 동기 Task 제공해 준다</summary>     /// <param name="task">큐에 대기할 Task입니다.</param>     protected sealed override void QueueTask(System.Threading.Tasks.Task task)     {         // 처리할 작업 목록에 추가.         // tasks가 처리중이거나 준비가 되지 않았을 때 대기 함         lock (_tasks)         {             _tasks.AddLast(task);             // MaximumConcurrencyLevel의 숫자보다 작을 때만 실행             // 동시성 수준을 제어하는 Scheduler 클래스 이므로 이곳에서 동시성 수준을 체크한다.             if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism)             {                 ++_delegatesQueuedOrRunning;                 NotifyThreadPoolOfPendingWork();             }         }     }     /// <summary>     /// 작업이 스케줄러에 대한 실행 할 필요가있는 스레드를 알려줍니다.     /// </summary>     private void NotifyThreadPoolOfPendingWork()     {         switch (ExecuteType)         {             case LimitedConcurrencyLevelTaskExecuteType.Thread:                 #region Thread로 실행                 // 쓰레드에서 Task가 실행이 되도록 한다.                 Thread thread = new Thread(new ThreadStart(() =>                 {                     NotifyThreadPoolOfPendingWorking();                 }));                 thread.Start();                 #endregion                 break;             case LimitedConcurrencyLevelTaskExecuteType.ThreadPool:                 #region ThreadPool.UnsafeQueueUserWorkItem로 실행                 // ThreadPool에서 실행이 되도록 한다.                 // 실행 주기는 ThreadPool이 CPU 환경에 맞게 동시 실행을 컨트롤 한다.                 // http://msdn.microsoft.com/ko-kr/library/system.threading.threadpool_methods(v=vs.110).aspx                 ThreadPool.UnsafeQueueUserWorkItem(_ =>                 {                     NotifyThreadPoolOfPendingWorking();                 }, null);                 #endregion                 break;         }     }     /// <summary>     /// 작업 실행을 시작 하도록 합니다.     /// </summary>     /// <remarks>     /// 실행을 Thread로 할것이지 ThreadPool에서 실행할 것인지 테스트를 위해 만듬.     /// </remarks>     private void NotifyThreadPoolOfPendingWorking()     {         // 현재 스레드가 작업 항목을 활성화 하도록 함         // 스레드에 작업 활성화를 할 수 있도록 한다.         // ThreadStatic으로 선언되었기에 쓰레드마다 별도의 값으로 접근 한다.         _currentThreadIsProcessingItems = true;         try         {             // 대기열에 사용 가능한 모든 항목을 처리합니다.             while (true)             {                 System.Threading.Tasks.Task item;                 lock (_tasks)                 {                     // 처리 할 항목이 더 있을 경우,                     // 처리가 완료되면 루프를 나간다.                     if (_tasks.Count == 0)                     {                         --_delegatesQueuedOrRunning;                         break;                     }                     // 큐에서 다음 항목을 가져 오기.                     item = _tasks.First.Value;                     _tasks.RemoveFirst();                 }                 // 큐에서 찾아낸 작업을 실행                 base.TryExecuteTask(item);             }         }         // 현재 스레드에서 처리 항목을 완료         finally { _currentThreadIsProcessingItems = false; }     }     /// <summary>연결된 쓰레드에서 동기 Task 제공해 준다</summary>     /// <param name="task">실행할 타스크</param>     /// <param name="taskWasPreviouslyQueued">작업이 이전에 큐에 대기되었는지 여부를 나타내는 부울입니다.이 매개 변수가 True이면 작업이 이전에 큐에 대기된 것일 수 있습니다. False이면 작업이 큐에 대기되지 않은 것입니다. 작업을 큐에 대기하지 않고 인라인으로 실행하려면 이 호출을 수행합니다.</param>     /// <returns>작업이 인라인으로 실행되었는지 여부를 나타내는 부울 값입니다. 성공적인 실행 시 True, 그 이외에 false</returns> /// <remarks>재진입으로 인한 오류를 방지하기 위해 작업 인라이닝은 관련된 스레드의 로컬 큐에서 대기 중인 대상이 있는 경우에만 발생합니다.</remarks>     protected sealed override bool TryExecuteTaskInline(System.Threading.Tasks.Task taskbool taskWasPreviouslyQueued)     {         //쓰레드에서 처리가 되고 있으면 별도 실해을 지정하지 않는다.         //중복 실행이 되지 않도록 해야 한다.         if (!_currentThreadIsProcessingItemsreturn false;         // 작업이 이전에 큐에 대기된 것이면 제거         if (taskWasPreviouslyQueuedTryDequeue(task);         // 한번더 실행을 시도 한다.         return base.TryExecuteTask(task);     }     /// <summary>이전에 이 스케줄러의 큐에 대기된 Task를 큐에서 제거하려고 합니다</summary>     /// <param name="task">큐에서 제거할 Task입니다.</param>     /// <returns>task 인수가 큐에서 제거되었는지 여부를 나타내는 부울입니다.</returns>     protected sealed override bool TryDequeue(System.Threading.Tasks.Task task)     {         lock (_tasksreturn _tasks.Remove(task);     }     /// <summary>이 TaskScheduler가 지원할 수 있는 최대 동시성 수준을 나타냅니다.</summary>     public sealed override int MaximumConcurrencyLevel { get { return _maxDegreeOfParallelism; } }     /// <summary>디버거를 지원하기 위해 현재 스케줄러의 큐에 대기되어 실행을 기다리고 있는 Task 인스턴스의 열거 가능한 형식을 생성합니다.</summary>     /// <returns>디버거가 현재 이 스케줄러의 큐에 대기된 작업을 트래버스할 수 있도록 허용하는 열거 가능한 형식입니다.</returns>     protected sealed override IEnumerable<System.Threading.Tasks.Task> GetScheduledTasks()     {         bool lockTaken = false;         try         {             Monitor.TryEnter(_tasksref lockTaken);             if (lockTakenreturn _tasks.ToArray();             else throw new NotSupportedException();         }         finally         {             if (lockTakenMonitor.Exit(_tasks);         }     } } /// <summary> /// 절대적인 우위를 보장하는 방법이 아니므로 두개 타입을 비교해서 항상 최선의 방법을 찾도로 해야 한다. /// </summary> public enum LimitedConcurrencyLevelTaskExecuteType {     /// <summary>     /// Thread로 Task를 실행 하도록 함.     /// ( CPU 경합이 적고, 대기 시간이 긴 로직에서 보다 많은 동시 수행을 진행 하도록 함 )     /// 절대적인 우위를 보장하는 방법이 아니므로 두개 타입을 비교해서 항상 최선의 방법을 찾도로 해야 한다.     /// </summary>     Thread,     /// <summary>     /// ThreadPool에서 Task를 실행 하도록 함.     /// </summary>     ThreadPool }

[코드3] Thread와 ThreadPool을 선택적으로 수행 하도록 수정한 코드


 위와 같이 소스를 수정하게 되었더니 동시 실행되는 Task가 Core 갯수의 제한 보다는 동시성 수준에 맞게 수행이 되었다. 하지만 일반적인 시나리오에서는 기본 스케줄러를 이용해 수행이 더 효율적이며 LimitedConcurrencyLevelTaskSchedler은 Cpu 경합 보다는 I/O 작업과 같은 대기 시간이 특히 오래 걸리는 작업에 대해 충분히 테스트를 거처 실 업무에 적용해 봐야 할 것이다. 이 코드는 모든 환경에서 우수한 성능으로 수행할 것이라고 보장하지 않는다. 이제 "코드4"을 통해 확인해 보자

/// <summary>
/// LimitedConcurrencyLevelTaskScheduler로 테스트
/// </summary>
public void LimitedConcurrencyLevelTaskScheduler_TestMethod()
{
    // 시간을 재기 위해서 사용
    Stopwatch sw = new Stopwatch();
    sw.Start();
 
    //var limitedScheduler = new LimitedConcurrencyLevelTaskScheduler(5, LimitedConcurrencyLevelTaskExecuteType.Thread);
    var limitedScheduler = new LimitedConcurrencyLevelTaskScheduler(5);
    limitedScheduler.ExecuteType = LimitedConcurrencyLevelTaskExecuteType.Thread;
 
    // 커스터마이징 된 LimitedConcurrencyLevelTaskScheduler을 이용해 TaskFactory를 생성 하도록 한다.
    var factory = new TaskFactory(limitedScheduler);
    var tasks = new List<System.Threading.Tasks.Task>();
 
    for (int j = 1j <= 20000j++)
    {
        var task = factory.StartNew(() =>
        {
            for (int i = 0i < 5i++)
            {
                var a = Thread.CurrentThread.ManagedThreadId;
                Console.WriteLine("{0} on thread {1}"iThread.CurrentThread.ManagedThreadId);
            }
        });
 
        tasks.Add(task);
    }
 
    // 모두 완료가 될 때까지 대기
    System.Threading.Tasks.Task.WaitAll(tasks.ToArray());
 
    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds + "ms");
}

[코드4] LimitedConcurrencyLevelTaskScheduler 테스트 코드


 지금까지 TaskScheduler을 커스터마이징 하는것에 대해서 알아 보았다. 그렇지만 모두 이해를 하는데에는 많이 모자랄 것이라 예상한다. 그건 아마도 필자의 실력이 아직 미진하여 정답을 제대로 알려 줄 수 있는 역량이 높지 않기 때문이리라 생각 한다. 그래서 좀더 깊이 있고 많이 알고 싶으신 분은 MS에서 제공하는 예제를 좀더 공부해 보면 어떨까 합니다. http://code.msdn.microsoft.com/windowsdesktop/Samples-for-Parallel-b4b76364

이 페이지의 샘플 소스를 받아 분석하고 자신의 자산으로 만들 수 있는 기회를 가졌으면 합니다. 지금까지 읽어 주셔서 감사합니다. 꼭 당부를 드리자면 직접 실행해 보고 디버깅을 해 보는 것이 눈으로 확인하는 것 보다 더 많은 기회와 가치를 제공해 준다는 것을 알려 드립니다.


소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다.. 실제 개발에서도 필요한 소스는 단순히 Copy & Paste 만으로도 사용할 수 있습니다. 그리고 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       


Task.ContinueWith



 Task 기반 비 동기 작업을 수행하다가 보면 순차적으로 수행해야 하는 작업이 있을 수 있다. 그럴때마다 await 키워드로 다시 비동기를 실행하지 않고 ContinueWith를 사용하면 순차적인 비 동기를 수행 할 수 있다. 아래 구문을 살펴 보자.


public async void TAP_AsyncAwait_TestMethod2()
{
    //.Net framework 4.5에 추가된 메소드로 
    //Task.Factory.StartNew 래핑한다.
    var returnValue = await Task.Run(() => { /* Something */ return 10; })
        //10을 리턴하면 아래 구문을 수행 한다.
        .ContinueWith(_result => { return _result.Result + 100; })
        //110을 리턴하면 아래 구문을 수행 한다.
        .ContinueWith(_return => { Thread.Sleep(1000); return _return.Result + 200; })
        //1초 후 310을 리턴하면 아래 구문을 수행 한다.
        //결과 값을 String으로 반환 한다.
        .ContinueWith(_return => { return _return.Result.ToString(); });
 
    Console.WriteLine(returnValue);       
}

[코드1] ContinueWith로 비 동기 순차 실행


 이와 같이 사용하여 순차적인 비 동기 실행을 구현하여 await를 남발하지 않도록 하면 될 것이다.



소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다.. 실제 개발에서도 필요한 소스는 단순히 Copy & Paste 만으로도 사용할 수 있습니다. 그리고 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       





Task.Run() 메소드


 .Net framework 4.5로 업그레이드 되면서 기존에 사용하고 있던 Task.Factory.StartNew() 메소드가 다음의 구문으로 쉽게 사용할 수 있도록 추가 되었다. 

//Factory를 통해 비 동기 Task를 반환
Task.Factory.StartNew(() => { /* Something */ });
 
//.Net framework 4.5에 추가된 메소드로 
//Task.Factory.StartNew 래핑한다.
Task.Run(() => { /* Something */ });

[코드1] 비 동기 실행 Task 수행 코드


 물론 위 코드도 Generic을 지원하고 있으며 .Net framework 4.5에서는 간결하게 래핑한 Run() 메소드를 사용하는걸 권장한다. 다만 일반 사용 보다는 정교하게 비 동기 작업에 대해서 컨트롤을 해야 할 때는 기존과 같은 방법으로 Factory의 StartNew를 사용해서 사용해야 할 것이다.



소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다.. 실제 개발에서도 필요한 소스는 단순히 Copy & Paste 만으로도 사용할 수 있습니다. 그리고 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       




C# Task-based Asynchronous Pattern - TAP(2)

async / await keyword

http://msdn.microsoft.com/ko-kr/library/hh873175.aspx

http://msdn.microsoft.com/en-us/library/hh873175.aspx


http://www.albahari.com/threading/part2.aspx#_Thread_Safety


2013/02/26 - [.Net Framework] - [Async]Task-based Asynchronous Pattern - TAP(1) Async #7


 이번 포스트는 지난 시간에 이어 TAP에 대해서 더 깊이 있게 알아 볼 수 있는 시간을 가져 보도록 하겠습니다. 지난 포스트의 마지막 부분에서 외부 시스템에서 정보를 가져오는( I/O, Network, ...)을 통해 예제를 통해 비 동기로 실행하면 총 수행 시간을 절약할 수 있는 방안에 대해 알아 보았다. 이 번에는 .Net framework 4.5가 나오면서 C#이 5.0으로 업그레이드가 되었다. 그러면서 비 동기 컨트롤을 지원하는 키워드인 async/await가 추가 되어 더욱 편리하고 안전하게 사용할 수 있게 되었다. 이제 새로 추가된 기능으로 비 동기 구현을 해 보도록 하자. 

/// <summary>
/// async/await 키워드를 이용해서 비 동기 실행
/// </summary>
[TestMethod]
public async void TAP_AsyncAwait_TestMethod1()
{
    //비 동기 실행을 하지만 결과값이 리턴이 될 때까지 대기 한다.
    var returnValue = await Task.Factory.StartNew(() => 
        {
            Debug.WriteLine("TAP_AsyncAwait_TestMethod1 비 동기 실행 시작");
            Thread.Sleep(1000);
            Debug.WriteLine("TAP_AsyncAwait_TestMethod1 비 동기 실행 종료");
            return 10;
        });
 
    //정상 수행 여부 체크
    Assert.AreEqual(10returnValue);
}

[코드1] async/await로 비 동기 실행

(테스트 프로젝트에서는 정상적으로 실행 되지 않아 아래 Console에서 확인 한다)


class Program
{
    static void Main(string[] args)
    {
        Program p = new Program();
        p.TAP_AsyncAwait_TestMethod1();
 
        Console.ReadKey();
    }
 
    public async void TAP_AsyncAwait_TestMethod1()
    {
        //비 동기 실행을 하지만 결과값이 리턴이 될 때까지 대기 한다.
        var returnValue = await Task.Factory.StartNew(() =>
        {
            Console.WriteLine("TAP_AsyncAwait_TestMethod1 비 동기 실행 시작");
            Thread.Sleep(1000);
            Console.WriteLine("TAP_AsyncAwait_TestMethod1 비 동기 실행 종료");
            return 10;
        });
 
        //값 확인
        Console.WriteLine(returnValue);
            
    

[코드2] Console에서 async/await로 비 동기 실행


 "코드2"에서와 같이 TAP_AsyncAwait_TestMethod 함수의 리턴 타입 앞에 'async' 키워드를 이용해서 이 함수는 비 동기로 수행 되는 메소드라고 알려줘야 한다 이렇게 해야지만 메소드 내부에서 await를 사용할 수 있다. 그리고 메소드 내부에서 비 동기로 수행되는 구문에서 await를 이용해서 결과값을 받아 오면 된다. 이렇게 간단한 규칙만으로 비 동기 실행을 절차적인 모양(기존 절차지향 언어에서 비 동기를 구현 할 때 Context를 맞춰줘야 하는 복잡한 지식 없이 절차 지향의 구문의 모양처럼 작성 하면서 메소드의 동작 방식은 비 동기로 구현 한다)으로 작성 하도록 도와 준다. 한번 직접 실행하여 분석해 보도록 하자. 생각 보다 쉽게 비 동기 지원 메소드를 작성 하고 사용할 수 있다. 


 이 기능은 프레임워크에서 지원되는 기능이 아닌 컴팔일러 단에서 지원하는 기능이며 내부적으로 TAP(1)에 설명된 사항 처럼 Task 객체를 이용해서 비 동기를 구현 하도록 내부적으로 구현 한다. .Net framework 4.5에서는 이렇게 쉽게 비 동기를 지원하는 편리한 기능을 추가 하였다. 그렇지만 모든 메소드를 비 동기로 작성해야 하는 필요성은 없으며 I/O, Network, DB와 같이 상대적으로 느린 시스템에서 데이터를 컨트롤 하는 부분에서만 비 동기를 사용할 것을 적극 권장하는 바이다.

C# Task-based Asynchronous Pattern - TAP

http://msdn.microsoft.com/ko-kr/library/hh873175.aspx

http://msdn.microsoft.com/en-us/library/hh873175.aspx


http://www.albahari.com/threading/part2.aspx#_Thread_Safety



 Task-based Asynchronous Pattern(이하 TAP)은 .Net framework 4.0에서 나온 개념으로 저 수준의 Thread를 컨트롤하지 않고 고 수준의 Task 레벨에서 비동기를 지원하고 있고 이것을 이용해 비동기 패턴 구현을 권장하고 있다.

 TAP은 단일 메소드를 사용하여 비동기 작업의 시작과 완료를 나타냅니다.이는 APM(Begin, End)과 대조적이다. TAP 메소드는 반환되는 형식에 따라 Task, Task<T>로 반환되고 Task<T>로 반환된 객체는 Result 필드를 통해서 반환된 값을 사용할 수 있다. 우선 간단하게 Task를 이용해서 비 동기 실행 코드를 살펴 보도록 하자. 

/// <summary>
/// TAP 비 동기 수행 테스트
/// </summary>
[TestMethod]
public void TAP_TestMethod_01()
{
    //간단한 비 동기 실행 수행
    Task task = new Task(() => 
    {
        Thread.Sleep(1000);
        Debug.WriteLine("비 동기 실행.");
    });
 
    //타스크를 비동기 실행 시킨다.
    task.Start();
 
    //타스크가 완료가 될 때까지 대기 한다.
    task.Wait();
 
    Debug.WriteLine("비 동기 실행 완료.");
 
    //강제로 성공이라고 표시한다.
    Assert.IsTrue(true);
}

[코드1] Task를 이용해 비 동기 수행 


 "코드1"을 보면 Task를 인스턴스 시키면서 파라메터로 Action을 넘겨준다. 그리고 인스턴스에서 Start()를 호출하여 비 동기로 작업을 수행한다. task.Wait()는 비 동기 작업이 완료되기 전에 메소드를 종료 하는것을 방지 하기 위해 대기하도록 설정하였다. 위와 같은 방법은 이전의 Thread와 별 다를 바 없을 것이다. 그렇다면 다음 코드를 한번 보도록 하자.

/// <summary>
/// Task의 Factory를 통해 TAP 비 동기 수행 테스트
/// </summary>
[TestMethod]
public void TAP_TestMethod_02()
{
    //Task의 Factory를 통해서 타스크를 받아 오는 방법
    var task = Task.Factory.StartNew(() =>
    {
        Thread.Sleep(1000);
        Debug.WriteLine("비 동기 실행.");
    });
 
    //타스크가 완료가 될 때까지 대기 한다.
    task.Wait();
 
    Debug.WriteLine("비 동기 실행 완료.");
 
    //강제로 성공이라고 표시한다.
    Assert.IsTrue(true);
}

[코드2] Task의 Factory를 통해 Task 생성 후 실행


 "코드2"는 "코드1"과 다른점이 task.Start() 없이 곧바로 수행이 되도록 하고 있다. 그럼 다시 다음 코드를 보고 비교 해  보도록 하자. 

/// <summary>
/// Task<![CDATA[<T>]]>의 Factory를 통해 TAP 비 동기 수행 테스트
/// </summary>
[TestMethod]
public void TAP_TestMethod_03()
{
    var tempValue = 100;
 
    //Task의 Factory를 통해서 타스크를 받아 오는 방법
    var task = Task.Factory.StartNew(() =>
    {
        Thread.Sleep(1000);
        Debug.WriteLine("비 동기 실행.");
 
        return 100;
    });
 
    //비 동기 실행에서 값이 반활 될때까지 대기하고 
    //반환이 되면 returnValue에 값을 바인딩 한다.
    var returnValue = task.Result;
 
    Debug.WriteLine("비 동기 실행 완료.");
 
    //값이 같으면 성공
    Assert.AreEqual(returnValuetempValue);
}

[코드3] Task<T>형식으로 비 동기 작업 수행 테스트


 "코드3"에서는 지금까지와는 다른 task.Result를 사용하였다. 이 구문은 비 동기로 수행되는 작업이 완료되어 값을 반환 할 때까지 대기하고 값을 할당 해준다. 그런데 왜 비동기를 사용해야 할까? 비 동기를 사용 하면 낮설고, 교착 상태에 빠질 수도 있으며 디버깅 하기도 더 어렵다. 그렇다면 아래 "코드4"를 한번 보길 바란다.

/// <summary>
/// Task<![CDATA[<T>]]>의 Factory를 통해 TAP 비 동기 수행 테스트
/// 여러 타스크를 비 동기로 수행하여 시간 절약을 얻었다.
/// </summary>
[TestMethod]
public void TAP_TestMethod_04()
{
    var tempValue = 450;
 
    Stopwatch sw = new Stopwatch();
    sw.Start();
 
    //Task의 Factory를 통해서 타스크를 받아 오는 방법
    var task1 = Task.Factory.StartNew(() =>
    {
        Thread.Sleep(2000);
        Debug.WriteLine("DB1.Server.com에서 DB를 가지고 옴.");
 
        return 50;
    });
 
    //Task의 Factory를 통해서 타스크를 받아 오는 방법
    var task2 = Task.Factory.StartNew(() =>
    {
        Thread.Sleep(3500);
        Debug.WriteLine("Socket로 레거시 시스템에서 정보를 가져 옴");
 
        return 150;
    });
 
    //Task의 Factory를 통해서 타스크를 받아 오는 방법
    var task3 = Task.Factory.StartNew(() =>
    {
        Thread.Sleep(1500);
        Debug.WriteLine("DB2.Server.com에서 DB를 저장하고 영향 받은 row 갯수를 가져옴.");
 
        return 250;
    });
 
    //비 동기 실행에서 값이 반활 될때까지 대기한다.
    Task.WaitAll(new[] { task1task2task3 });
 
    sw.Stop();
    Debug.WriteLine(sw.ElapsedMilliseconds);  //3504밀리초가 output에 찍혔다.
    Debug.WriteLine("비 동기 실행 완료.");
 
    //returnValue에 비 동기로 수행해 받은 값을 바인딩 한다.
    var returnValue = task1.Result + task2.Result + task3.Result;
 
    //값이 같으면 성공
    Assert.AreEqual(returnValuetempValue);
}

[코드4] 비 동기 수행으로 전체 수행 시간을 빠르게 할 수 있다.


 위 코드를 살펴 보면 외부 시스템(DB, Socket)으로 정보를 가져오고 저장하는 프로세스가 있다. 각 수행 시간이 2000, 3500, 1500 밀리초가 걸린다고 가졍하며 코딩을 하였다. 이 작업을 순차적으로 수행하면 2000 + 3500 + 1500을 모두 합한 7초가 걸린다는 산술적인 계산이 떨어 진다. 그렇지만 위 작업을 비 동기로 수행하면 최대 공약수인 3500밀리초만 걸리다는 것을 알 수 있다. 위 코드에서 sw.ElapsedMilliseconds 코드의 결과값이 3504(컴퓨터 마다 약간의 차이가 발생한다)로 측정이 되었다. 7초와 3.5초라는 단순 비교 만으로도 비 동기 수행을 함으로써 얻을 수 있는 잇점을 장황한 설명을 하지 않아도 알 수 있을 것이다.



Tip!

나중에 ASP.NET MVC에서 IAsyncController을 통해서 비동기 컨트롤을 사용하여 얻을 수 있는 잇점에 대해서도 상세하게 다루도록 하겠다.




 지금까지 TAP를 이용해서 비 동기 작업에 대해서 간단하게 탐색해 보았다. 다음 포스트에서는 지금보다 더 상세하게 알아보는 시간을 가져보도록 하겠다.


------------------------------------------------------------------------------------------


 아래는 MS에서 배포하고 있는 TAP관련 문서이며 번역번도 같이 올려 놓는다. - 번역본은 판교 (이전 가락동)에서 교육을 받을 때 강습생들이 번역한 문서다.


TAP(Task-based Asynchronous Pattern).docx

TAP(Task-based Asynchronous Pattern)(번역본)completed.docx


C# Event-based Asynchronous Pattern - EAP(4)

이벤트 기반 비동기 프로그래밍 패턴


http://msdn.microsoft.com/en-us/library/ms228969.aspx



지난 포스트

 지난 시간 포스트에서 thinkingAsync.Completed += thinkingAsync_Completed; 코드로 이벤트 핸들러를 등록하여 완료 이벤트가 발생 하였을 때 해당 핸들러 메소드에서 처리토록 하였다. 여기까지도 훌륭하다. 그렇지만 해당 핸들러의 메소드에서 받는 파라메터의 형식이 좀 아쉽다. 아래 "코드1"에서 보면 두번째 파라메터에서 받는 타입이 .Net Framework에서 제공하는 일반적인 기본 클래스를 넘겨 주도록 되어 있다.

void thinkingAsync_Completed(object senderAsyncCompletedEventArgs args)
{
    //취소 여부
    var a = args.Cancelled;
    //에러 메세지
    var b = args.Error;
    //멀티로 수행 시 구분 값
    var c = args.UserState;
}

[코드1] 비 동기 수행 완료시 실행되는 메소드


 해당 메소드의 코드를 보시면 세가지 타입만 얻어 올 수 있도록 제한이 되어 있다. EAP에서는 권고 사항으로 이벤트가 수행할 때 넘겨 받는 파라메터에서 형변환을 하지 않고 사용할 수 있도록 제공하는 것을 지향하고 있다. 그럼으로 이번 코드에서는 Generic 형식으로 만들어 보고 Action 형시으로 구성된 사항을 Func<T>로 재 구성해 보도록 하겠다. 지금까지의 코딩은 넘겨 받는 값이 없을 때 (void 타입) 사용하면 되는 케이스고 이번에는 비 동기 수행 후 넘겨 받는 파라메터가 있을 경우 사용하면 될 것이다. "코드2"를 보도록 하자.

/// <summary>
/// 생각하는 비동기 클래스 EAP - Generic class
/// </summary>
/// <remarks>
/// EAP로 개발하기 준비 단계
/// 클래스 내부에서는 APM 형식으로 비동기를 수행하고
/// 클래스 외부에서는 Event 형식으로 수행이 되도록 지원해야 한다.
/// 현재 코드는 아직 Event를 연결하지 않고 비동기 수행이 되는지 여부만 구현 하였다.
/// </remarks>
/// <example> 비 동기 수행 완료 이벤트 사용 예제
/// <code>
/// ThinkingAsyncEAP<int> thinkingAsync = new ThinkingAsyncEAP<int>();
/// thinkingAsync.Completed += thinkingAsync_Completed;
/// var result = thinkingAsync.StartAsync(() => { return 1;});
/// </code>
/// </example>
public class ThinkingAsyncEAP<T>
{
    /// <summary>
    /// 새로운 클래스에 맞는 Arguments의 타입을 만들기 위해 상속받아 재 정의.
    /// </summary>
    /// <remarks>
    /// inner class로 선언하여 한번에 Generic 클래스를 사용할 수 있도록 함.
    /// </remarks>
    public class ThinkingAsyncCompletedEventArgs : AsyncCompletedEventArgs
    {
        /// <summary>
        /// 사용자 정의 타입 값
        /// </summary>
        public T Result { getset; }
 
        /// <summary>
        /// 생성자에서는 별 기능을 하지 않고 바로 base 클래스로 넘겨 준다.
        /// </summary>
        /// <param name="error">에러 사항</param>
        /// <param name="cancelled">취소 여부</param>
        /// <param name="userState">쓰레드 구분자</param>
        public ThinkingAsyncCompletedEventArgs(Exception errorbool cancelledobject userState) : base(errorcancelleduserState)
        {
 
        }
    }
 
    //delegate를 통해 입력 파라메터를 선언한다.
    public delegate void ObjectCompletedEventHandler(object senderThinkingAsyncCompletedEventArgs args);
    //선언된 delegate를 통해 event를 선언한다.
    public event ObjectCompletedEventHandler Completed;
 
    /// <summary>
    /// 비동기 수행 시작하기
    /// </summary>
    /// <param name="func">action, delegate parameter</param>
    /// <returns></returns>
    public IAsyncResult StartAsync(Func<int> func)
    {
        //APM 형식처럼 비동기로 실행 한다.
        //delegate의 BeginInvoke와 동일 한다.
        var asyncHandlerPointer = func.BeginInvoke(new AsyncCallback(AsyncCompleted), func);
 
        //IAsyncResult를 반환 한다.
        //APM에서 사용하던 기존 코드와의 호환성을 유지하기 위해 반환한다.
        //호환성 유지가 필요 없으면 반환하지 않고 메소드를 void 형식으로 선언하면 된다.
        return asyncHandlerPointer;
    }
 
    /// <summary>
    /// 비동기 수행 종료하기
    /// </summary>
    /// <param name="result">IAsyncResult</param>
    public void AsyncCompleted(IAsyncResult result)
    {
        ManualResetEvent manualResetEvent = (ManualResetEvent)result.AsyncWaitHandle;
        //EndInvoke를 통해서 값을 넘겨 받고 대기자를 완료 한다.
        //manualResetEvent.Set()도 자동으로 수행이 된다.
        var tempResult = ((Func<T>)result.AsyncState).EndInvoke(result);

        //var asyncResult = (AsyncResult)result;
        ////BeginInvoke시 object를 넘겨 주지 안았을 경우 아래와 같은 방식으로도 실행자를 가져올 수 있다.
        //var tempResult = ((Func<T>)asyncResult.AsyncDelegate).EndInvoke(result);
        //OnCompleted가 다른 곳에서 subscription되었는지 파악한다.         //subscription하는 방법은 "OnCompleted += ThinkingAsyncEAP_OnCompleted;" 이다.         if (Completed != null)         {             //사용자 정의 이벤트일때 해당 파라메터를 세팅한다.             //.Net에서 기본적으로 제공하는 파라메터이다.             var args = new ThinkingAsyncCompletedEventArgs(nullfalsenull) {  Result = tempResult };             //Completed를 실행하면 연결된 이벤트 핸들러로 제어권이 넘어 간다.             Completed(thisargs);         }     } }

[코드2] Thinking Generic class


 위 "코드2"는 이전 포스트에서 알려 드렸던 코드에서 레이아웃에서 전체 흐름상으로는 크게 변경 되지 안았으나 반환값이 있어야 한다는 제약 조건 때문에 위와 같이 코드를 작성 하였다. Action대신 Func를 사용하였고 AsyncCompletedEventArgs를 상속받아 ThinkingAsyncCompletedEventArgs를 구현하였으며 비동기 수행 코드를 약간 수정하여 반환 할 수 있도록 하였다. 지금까지의 코드 자체의 완결성은 온전하지 않은 상태이며 개념을 이해 시키고자 하는 목적으로 코드를 작성 하였다. 이제 이 코드를 사용하는 테스트를 보도록 하자.

/// <summary>
/// 이벤트 기반 비동기 테스트
/// </summary>
[TestMethod]
public void Thinking_Event_Base_EAP_ASync_UnitTest()
{
    //Arrange step
    //테스트 확인용 변수
    int tempI = 0tempJ = 100;
 
    //Act step
    ThinkingAsyncEAP<int> thinkingAsync = new ThinkingAsyncEAP<int>();
    //비 동기 수행 완료가 되었을 때 호출 할 이벤트
    thinkingAsync.Completed += thinkingAsync_Completed;
    var result = thinkingAsync.StartAsync(() =>
    {
        int i = 1;
        for (; i <= 100i++)
        {
            //50 밀리초씩 생각 한다.
            Thread.Sleep(50);
            Debug.WriteLine(i);
            tempI = i;
        }
 
        return i;
    });
 
    //테스트를 확인하기 위해 비동기가 완료 할 때까지 대기.
    result.AsyncWaitHandle.WaitOne();
 
    //Assert step
    //값이 맞는지 테스트
    Assert.AreEqual(tempItempJ);
}
 
void thinkingAsync_Completed(object senderThinkingAsyncEAP<int>.ThinkingAsyncCompletedEventArgs args)
{
    //비 동기 수행이 완료가 되었습니다.
 
    //취소 여부
    var a = args.Cancelled;
    //에러 메세지
    var b = args.Error;
    //멀티로 수행 시 구분 값
    var c = args.UserState;
 
    //사용자 정의된 값
    var d = args.Result;    //101이 반환되지만 맞는 값이다. (for 구문때문에)
}

[코드3] 이벤트 기반 비동기 수행 테스트 코드


 "코드3"에서와 같이 수행 할 코드에서 나온 예제 형식으로 EAP 형식의 클래스를 선언하고 사용할 수 있다. 참고로 Inner class에 대해서는 http://blog.spowner.com/56 에서 설명을 읽어 보셔서 참고 하시면 되겠습니다.


 초반 예상보다 많은 포스트를 통해서 EAP에 대해 설명을 할 수 밖에 없었는데요. 라이브러리 개발자나 UI가 있는 상황에서 비 동기로 개발하면 일반 개발자가 사용 할 때 하나의 가이드 라인으로 제약할 수 있을것입니다. 그리고 이벤트는 기반으로 라이브러리르 구축함으로써 서로 다른 모듈간의 확장성을 확보할 수 있게 할 수 있습니다. 지금까지의 부족한 점이 많았지만 예제 및 주석, 간단한 설명을 통해 개념을 알려 드리고자 노력 하였습니다. 그렇지만 부족한 글 솜씨와 저의 한계로 많은 부분에서 미약한 부분이 많았으리라 생각이 듭니다. 그래도 최대한 노력하며 자체 검증을 통해 오류가 없도록 하였습니다. 감사합니다.

C# Event-based Asynchronous Pattern - EAP(3)

이벤트 기반 비동기 프로그래밍 패턴


http://msdn.microsoft.com/en-us/library/ms228969.aspx



지난 포스트

 지난 포스트에서 EAP 라이브러리 만들기가 길어져서 중간 단계까지만 진행되었다. 이번에는 그 다음을 잰행해 보도록 하자. 


 이번 부터는 이전에 얘기 했던 delegate와 event를 가지고 Event를 발생시키는 클래스를 추가해야 한다. 이벤트 지향 프로그램은 UI가 있는 Platform에서 사용하기 쉽도록 하기 위해 Visual Basic에서 부터 도입이 된 방식이며 확장성 및 필터 개념처럼 수행되어 실행중에 이벤트를 이용해서 본래 데이터를 변경 및 추가 작업이 용이하게 할 수 있도록 지원한다. ASP.NET, Window, WPF의 기본 이벤트에서 "Loaded" 메소드가 있는 것을 알 수 있을 것이다. 이 메소드 또한 이벤트 방식으로 실행이 되며 우리는 이 이벤트에서 사용할 변수 초기화 또는 별도의 로직을 수행하는 코드를 작성하여 개발 한다. 이와 같이 현재는 이벤트 기반 개발 방식이 많은 Platform(ASP.NET, ASP.Net MVC, WinForm, WPF, Silverlight, ...)에서 사용되고 있는 방식이다.


 이제는 사용자 정의 event를 만드는 방법에 대해 간략하게 알아 보도록 하겠다. event는 delegate를 통해서 만들수 있으며 event를 코드에서 다시 선언 및 호출하여 사용자 정의 이벤트를 발생시킬 수 있다.

//delegate를 통해 입력 파라메터를 선언한다. private delegate void ObjectCompletedEventHandler(object senderAsyncCompletedEventArgs args); //선언된 delegate를 통해 event를 선언한다. private event ObjectCompletedEventHandler Completed;

[코드 1] delegate오 event 선언


"코드1"에서와 같이 선언하면 해당 클래스에서 "코드2"에서와 같이 사용할 수 있다.

//OnCompleted가 다른 곳에서 subscription되었는지 파악한다. //subscription하는 방법은 "OnCompleted += ThinkingAsyncEAP_OnCompleted;" 이다. if (Completed != null) {     //사용자 정의 이벤트일때 해당 파라메터를 세팅한다.     //.Net에서 기본적으로 제공하는 파라메터이다.     var args = new AsyncCompletedEventArgs(nullfalsenull);     //Completed를 실행하면 연결된 이벤트 핸들러로 제어권이 넘어 간다.     Completed(thisargs); }

[코드2] 사용자 정의 event 발생 시키기


 Completed의 선언된 이벤트는 Completed(this, args)와 같이 선언된 형식으로 호출 함으로써 발생 시킬 수 있으며 이벤트 변수는 꼭 null 체크를 해야 한다. 이벤트 객체는 클래스 외부에서 사용하도록 노출 시켜주는 특별한 delegate다. 그래서 해당 이벤트를 Subscription으로 등록하지 않으면 에러가 발생한다. 아래 "코드3"은 추가된 코드를 포함 한 전체 소스 코드이다.

/// <summary> /// 생각하는 비동기 클래스 EAP /// </summary> /// <remarks> /// EAP로 개발하기 준비 단계 /// 클래스 내부에서는 APM 형식으로 비동기를 수행하고 /// 클래스 외부에서는 Event 형식으로 수행이 되도록 지원해야 한다. /// 현재 코드는 아직 Event를 연결하지 않고 비동기 수행이 되는지 여부만 구현 하였다. /// </remarks> public class ThinkingAsyncEAP {     //delegate를 통해 입력 파라메터를 선언한다.     public delegate void ObjectCompletedEventHandler(object senderAsyncCompletedEventArgs args);     //선언된 delegate를 통해 event를 선언한다.     public event ObjectCompletedEventHandler Completed;     /// <summary>     /// 비동기 수행 시작하기     /// </summary>     /// <param name="action">action, delegate parameter</param>     /// <returns></returns>     public IAsyncResult StartAsync(Action action)     {         //APM 형식처럼 비동기로 실행 한다.         //delegate의 BeginInvoke와 동일 한다.         var asyncHandlerPointer = action.BeginInvoke(new AsyncCallback(AsyncCompleted), action);         //IAsyncResult를 반환 한다.         //APM에서 사용하던 기존 코드와의 호환성을 유지하기 위해 반환한다.         //호환성 유지가 필요 없으면 반환하지 않고 메소드를 void 형식으로 선언하면 된다.         return asyncHandlerPointer;     }     /// <summary>     /// 비동기 수행 종료하기     /// </summary>     /// <param name="result">IAsyncResult</param>     public void AsyncCompleted(IAsyncResult result)     {         //Action action = (Action)result.AsyncState;         //action.EndInvoke(result);        ManualResetEvent manualResetEvent = (ManualResetEvent)result.AsyncWaitHandle;         //OnCompleted가 다른 곳에서 subscription되었는지 파악한다.         //subscription하는 방법은 "OnCompleted += ThinkingAsyncEAP_OnCompleted;" 이다.         if (Completed != null)         {             //사용자 정의 이벤트일때 해당 파라메터를 세팅한다.             //.Net에서 기본적으로 제공하는 파라메터이다.             var args = new AsyncCompletedEventArgs(nullfalsenull);             //Completed를 실행하면 연결된 이벤트 핸들러로 제어권이 넘어 간다.             Completed(thisargs);         }

       manualResetEvent.Set(); //위치 변경     } }

[코드3] ThinkingAsyncEAP 업데이트 된 소스 코드


 "코드3"에서 음영 표시된이 이전코드에서 갱신된 코드다. 비 동기 수행이 완료가 되었을 때 클래스 외부로 사용자 정의 이벤트를 노출 시켜주는 기능을 추가 하였다. 이제 수정된 코드를 사용하는 법을 테스트 코드를 통해서 알아 보도록 하자.

/// <summary> /// 이벤트 기반 비동기 테스트 /// </summary> [TestMethod] public void Thinking_Event_Base_EAP_ASync_UnitTest() {     //Arrange step     //테스트 확인용 변수     int tempI = 0tempJ = 100;     //Act step     ThinkingAsyncEAP thinkingAsync = new ThinkingAsyncEAP();     //비 동기 수행 완료가 되었을 때 호출 할 이벤트     thinkingAsync.Completed += thinkingAsync_Completed;     var result = thinkingAsync.StartAsync(() =>     {         for (int i = 1i <= 100i++)         {             //50 밀리초씩 생각 한다.             Thread.Sleep(50);             Debug.WriteLine(i);             tempI = i;         }     });     //테스트를 확인하기 위해 비동기가 완료 할 때까지 대기. IAsyncResult를 리턴하도록 APM 환되도록 하여 사용할 수 있는 방안이다.     result.AsyncWaitHandle.WaitOne();     //Assert step     //값이 맞는지 테스트     Assert.AreEqual(tempItempJ); } void thinkingAsync_Completed(object senderAsyncCompletedEventArgs args) {     //비 동기 수행이 완료가 되었습니다. }

[코드4] 이벤트 기반 비동기 수행 테스트 코드


 "코드4" 추가된 이벤트 기능을 이용해 테스트를 하는 코드이며 thinkingAync.Completed += handlerMethod를 통해 완료할 때 실행될 핸들러를 등록 하였다. 수행 후 완료가 되면 해당 메소드를 실행하고 완료가 된다. 테스트 메소드에서의 코딩은 하나의 메소드에서 실행 및 테스트를 하기 위해 위와 같이 WaitOne() 메소드를 하출 하도록 하였지만 실제 코딩에서는 Completed 핸들러 메소드에서만 실행이 되도록 하면 되겠다.


 이번에도 코드가 너무 길어 지고 장황스럽게 되어 다음 포스트에서 이어 가도록 하겠다.







C# Event-based Asynchronous Pattern - EAP(2)

이벤트 기반 비동기 프로그래밍 패턴


http://msdn.microsoft.com/en-us/library/ms228969.aspx



지난 포스트 


 이번 포스트는 지난 시간이 이은 두번째 EAP로 라이브러리를  알아 보도록 하겠습니다. 우선 EAP로 개발하기 위해서는 delegate와 event에 대해서 기본적인 선 지식이 있어야 이해가 편할 것으로 보이지만 전혀 모르는 상태에서도 전체적인 구성을 이해에는 별 무리가 없을 것입니다.


 다중 동시 작업 지원

한번에 한 작업만 지원 

 void MethodNameAsync([object userState]);  void MethodNameAsync();
 void CancelAsync([object userState]);  void CancelAsync();


 위와 같은 메소드로 비동기 수행을 실행할 수 있도록 규칙을 따라 함수를 만들어야 한다. ( Async의 object 파라메터는 옵션이다.) 자 이제 직접 코드로 살펴 보도록 하자. 하지만 아직까지는 Event 기반 클래스는 아니며 EAP를 만들기 위한 전 단계를 만들어 EAP로 가기 위한 전체적인 흐름을 쉽게 알 수 있도록 하겠다. 점차 점진적으로 EAP를 만들어 가 보자.

/// <summary>
/// 생각하는 비동기 클래스 EAP
/// </summary>
/// <remarks>
/// EAP로 개발하기 준비 단계
/// 클래스 내부에서는 APM 형식으로 비동기를 수행하고
/// 클래스 외부에서는 Event 형식으로 수행이 되도록 지원해야 한다.
/// 현재 코드는 아직 Event를 연결하지 않고 비동기 수행이 되는지 여부만 구현 하였다.
/// </remarks>
public class ThinkingAsyncEAP
{
    /// <summary>
    /// 비동기 수행 시작하기
    /// </summary>
    /// <param name="action">action, delegate parameter</param>
    /// <returns></returns>
    public IAsyncResult StartAsync(Action action)
    {
        //APM 형식처럼 비동기로 실행 한다.
        //delegate의 BeginInvoke와 동일 한다.
        var asyncHandlerPointer = action.BeginInvoke(new AsyncCallback(Completed), action);
 
        //IAsyncResult를 반환 한다.
        //APM에서 사용하던 기존 코드와의 호환성을 유지하기 위해 반환한다.
        //호환성 유지가 필요 없으면 반환하지 않고 메소드를 void 형식으로 선언하면 된다.
        return asyncHandlerPointer;
    }
 
    /// <summary>
    /// 비동기 수행 종료하기
    /// </summary>
    /// <param name="result">IAsyncResult</param>
    public void Completed(IAsyncResult result)
    {
        Action action = (Action)result.AsyncState;
        action.EndInvoke(result);
        //ManualResetEvent d = (ManualResetEvent)result.AsyncWaitHandle;
        //d.Set();
    }
}

[코드1] ThinkingAsyncEAP #1


 "코드 1"에서 delegate의 BeginInvoke를 이용하여 비동기 실행이 되도록 하였으며 IAsyncResult를 반환하여 기존에 APM으로 작성된 코드와도 호환이 되도록 하였다. 우선 코드 설명으로는 기본적으로 주석으로 대부분 설명 하였으며 전체적인 흐름은 StartAsync를 통해 비동기 실행한 구문을 받아 수행하고 완료가 되면 Completed 메소드가 수행하여 비동기 수행을 종료토록 한다. 다시 설명하지만 아직은 이벤트 기반으로 작성된 코드는 아니면 여기까지의 코드를 가지고 테스트에서 정상적으로 수행이 되는지 확인해 보도록 하자. 테스트 코드는 아래 "코드2"에 있다. 생각보다 시 수행 코드보다 테스트에서 확인하는 코드의 길이가 훨씬 긴 편인데 여러가지 케이스에 대해서 한번에 확인하기 위해 코드가 길어 졌다. 그렇지만 테스트 함수에서 사용한 코드를 통해 어떻게 사용하는지에 대해서 습득할 수 있는 여러가지 기법들을 알 수 있는 기회가 될 것이므로 이 역시 꼼꼼히 살펴 보도록 하자.

//Thinking메소드에서 사용할 변수(Assert 단계에서 정상 수행 확인 용도)
private int tempI2 = 0;
 
/// <summary>
/// EAP Library 만들기 #1
/// </summary>
/// <remarks>
/// TDD의 Unit Test는 하나의 메소드로 하나의 방법에 대해서만 테스트 하도록 권장하고 있다.
/// 그렇지만 여러가지 방안을 한번에 확인하고 쉽게 접근 및 이해가 편하도록 실행 여러가지 케이스를 한번에 Act, Assert 하도록 하였다.
/// </remarks>
[TestMethod]
public void Thinking_EAP_ASync_UnitTest()
{
    //Arrange step
    //테스트 확인용 변수
    int tempI = 0tempJ = 100;
 
    #region Action case
    //Act step
    var thinking1 = new ThinkingAsyncEAP();
    //별도의 쓰레드에서 실행
    var asyncHandlerPointer1 = thinking1.StartAsync(() =>
    {
        for (int i = 1i <= 100i++)
        {
            //50 밀리초씩 생각 한다.
            Thread.Sleep(50);
            Debug.WriteLine(i);
            tempI = i;
        }
    });
 
    Action action = () =>
    {
        for (int i = 1i <= 100i++)
        {
            //50 밀리초씩 생각 한다.
            Thread.Sleep(50);
            Debug.WriteLine(i);
            tempI = i;
        }
    };
 
    var thinking2 = new ThinkingAsyncEAP();
    //별도의 쓰레드에서 실행
    var asyncHandlerPointer2 = thinking2.StartAsync(action);
    #endregion
 
    #region delegate case
    var thinking3 = new ThinkingAsyncEAP();
    //별도의 쓰레드에서 실행
    var asyncHandlerPointer3 = thinking3.StartAsync(delegate()
    {
        for (int i = 1i <= 100i++)
        {
            //50 밀리초씩 생각 한다.
            Thread.Sleep(50);
            Debug.WriteLine(i);
            tempI = i;
        }
    });
    #endregion
 
    #region delegate case 2
    var thinking4 = new ThinkingAsyncEAP();
    //별도의 쓰레드에서 실행
    var asyncHandlerPointer4 = thinking4.StartAsync(Thinking);
    #endregion
 
    #region 별도의 로직
    //별도의 다른 로직 수행 시작
 
 
    //별도의 다른 로직 수행 종료
    #endregion
 
    ////대기가 필요 하면 WaitOne를 통해 쓰레드가 종료 될때까지 기다리고
    ////필요 없으면 주석처리 후 메소드를 종료 시키면 된다.
    //asyncHandlerPointer1.AsyncWaitHandle.WaitOne();
    //asyncHandlerPointer2.AsyncWaitHandle.WaitOne();
    //asyncHandlerPointer3.AsyncWaitHandle.WaitOne();
    //asyncHandlerPointer4.AsyncWaitHandle.WaitOne();
 
    //한번에 대기 하기 - 처리가 완료 될때까지 대기 한다. - 여기서는 assert단계에서 값을 확인하기 위해 대기하도록 하였다.
    //new[]는 컬렉션으로 만들어 주는 .net framework 3.5 문법이다.
    WaitHandle.WaitAll(new[]
    { 
        asyncHandlerPointer1.AsyncWaitHandle, 
        asyncHandlerPointer2.AsyncWaitHandle, 
        asyncHandlerPointer3.AsyncWaitHandle,
        asyncHandlerPointer4.AsyncWaitHandle
    });
 
    //Assert step
    //값이 맞는지 테스트
    //thread unsafe 하지만 마지막 실행 쓰레드가 
    //100으로 할당함으로 별도의 추가 작업을 하지 않겠음
    Assert.AreEqual(tempItempJ);
 
    //Assert step
    //thinking3.StartAsync(Thinking)는 closure 함수로 치환되지 않기에 
    //전역 함수를 사용해서 체크를 할 수 밖에 없었다.
    Assert.AreEqual(tempI2tempJ);
 
    //action, func, anomymouse delegate 함수는 컴파일러의 의해서
    //local 변수를 함수 내부로 포함되게 치환되어 사용할 수 있도록 된다.
    //그래서 아래와 같은 코드가 정상적으로 수행이 되는 것이다.
    //int i = 0;
    //() => { i++; } or delegate(){ i++; } 와 같이 선언되어서 바깥에서 선언된
    //i의 값을 내부에서 사용할 수 있도록 지원 한다.
}
 
/// <summary>
/// 시간끌기용 더미 메소드
/// </summary>
public void Thinking()
{
    for (int i = 1i <= 100i++)
    {
        //50초씩 생각 한다.
        Thread.Sleep(50);
        Debug.WriteLine(i);
        tempI2 = i;
    }
}

[코드 2] ThinkingAsyncEAP Unit Test


 위 "코드2"에서는 delegate, action, method point를 통해서 테스트를 진행하였으며 비동기로 수행할 로직은 단순하게 1부터 100까지 증가하는 로직을 테스트 하였다. 그리고 Debug.WriteLine를 통해 Visual studio의 "Output"창에서 확인 할 수 있도록 하였다. 그리고 한가지 더 덧 붙이자면 tempI2는 Clousure 함수와 같이 외부에서 선언된 메소드를 별도의 클래스의 내부에서 사용할 수 있는 방법이 없기 때문에 전역변수로 선언하여 사용 하여 테스트를 하였다.


 이번 포스트를 진행하다 보니 너무 길어 지는 문제점이 있엇 다음 포스트에서 계속 이어 가도록 하겠다. 너무 길어 지면 집중도와 이해가 낮아 지므로 다음에 계속 이어 가도록 하겠다.

+ Recent posts