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™       



Ajax Control Toolkit Release


참조 URL
  1. Ajax Control Toolkit
  2. http://www.asp.net/ajaxLibrary/AjaxControlToolkitSampleSite/Default.aspx
  3. Tutorials

 

 ASP.NET MVC 기반에서 개발을 하고 있다면 한번 사용해 보자. 오래전 부터 개발이 진행되고 있었고 최근에 릴리즈가 되었다. Ajax 관련 서버 컨트롤들을 쉽게 사용할 수 있도록 지원하는 컨트롤이다.




Assembly Version Loading

참조 URL
  1. AssemblyName.Version 속성



 현재 어셈블리의 버전을 읽는 방법을 아래와 같이 소개 한다.


[코드] Assembly Version load



Microsoft Enterprise Library 6


참조 URL
  1. http://entlib.codeplex.com/
  2. http://msdn.microsoft.com/en-us/library/dn169621.aspx
  3. Download
  4. http://www.sqler.com/375933
  5. http://channel9.msdn.com/Tags/entlib
  6. Microsoft Enterprise Library 6 and Unity 3 Released

 


Microsoft Enterprise library 도 이번에 6로 업데이트가 되었다. 기업용 솔루션을 만들거나 서버 프로그램을 개발한다면 사용해 보는것도 괜찮을 것이다. 적어도 분석을 통해서 개발에 대한 가이드에 대해서 생각해 보는 시간이 될 것이라 생각한다. 



Overview

Enterprise Library consists of reusable software components that are designed to assist developers with common enterprise development challenges. It includes a collection of functional application blocks addressing specific cross-cutting concerns such as data access, logging, or validation; and wiring blocks, Unity and the Interception/Policy Injection Application Block, designed to help implement more loosely coupled, testable, and maintainable software systems.

Different applications have different requirements, and you will find that not every application block is useful in every application that you build. Before using an application block, you should have a good understanding of your application requirements and of the scenarios that the application block is designed to address.

Microsoft Enterprise Library 6 contains the following application blocks:

  • Data Access Application Block. Developers can use this application block to incorporate standard database functionality in their applications, including both synchronous and asynchronous data access and returning data in a range of formats.
  • Exception Handling Application Block. Developers and policy makers can use this application block to create a consistent strategy for processing exceptions that occur throughout the architectural layers of enterprise applications.
  • Logging Application Block. Developers can use this application block to include logging functionality for a wide range of logging targets in their applications. This release adds asynchronous logging capabilities.
  • Policy Injection Application Block. Powered by the Interception mechanism built into Unity, this application block can be used to implement interception policies to streamline the implementation of common features, such as logging, caching, exception handling, and validation, across a system.
  • Semantic Logging Application Block. This application block provides a set of destinations (sinks) to persist application events published using a subclass of the EventSource class from the System.Diagnostics.Tracing namespace. Sinks include Windows Azure table storage, SQL Server databases, and flat files with several formats and rolling capabilities. Developers can extend the block by creating custom formatters and sinks. For those sinks that can store structured data, the block preserves the full structure of the event payload in order to facilitate analyzing or processing the logged data. Events can be persisted in-process or collected and persisted out-of-process in a separate service.
  • Transient Fault Handling Application Block. This application block makes on-premises or cloud applications more resilient to transient failures by providing intelligent retry logic mechanisms.
  • Unity Application Block. Developers can use this application block as a lightweight and extensible dependency injection container with support for constructor, property, and method call injection, as well as instance and type interception. This release adds support for Windows Store apps as well as the registration by convention feature to ease the task of configuring Unity.
  • Validation Application Block. Developers can use this application block to create validation rules for business objects that can be used across different layers of their applications.

Enterprise Library also includes a set of core functions for declarative configuration support.



What is Enterprise Library?

Microsoft Enterprise Library is a popular collection of reusable software components (called application blocks) designed to address common cross-cutting concerns of enterprise application developers (such as logging, validation, data access, exception handling, and more). Enterprise Library is provided as source code, test cases, and documentation that can be used "as is" or extended, and encapsulates the Microsoft recommended and proven practices for .NET application development.

Unity is one of the Enterprise Library application blocks which provides a lightweight, extensible dependency injection container with support for constructor, property, and method call injection, as well as support for instance and type interception. It facilitates building loosely coupled applications (including Windows Store apps).

Enterprise Library can be useful in a variety of situations:

  • Enterprise Library provides sufficient functionality to support many common cross-cutting scenarios that enterprise-level applications must address.
  • Enterprise Library can serve as the basis for a custom library. You can take advantage of the extensibility points incorporated in each application block and extend the application block by adding new providers. You can also modify the source code for the existing application blocks to incorporate new functionality, and even add new application blocks to Enterprise Library. You can either develop extensions for existing application blocks and new application blocks yourself, or you can use extensions and application blocks developed by others.
  • Enterprise Library is designed so that its application blocks can function independently of each other. You need to add only the application blocks that your application will use.
  • Enterprise Library includes the source code and the unit tests for all application blocks. This means you can explore the implementations, modify the application blocks to merge into your existing library, or you can use parts of the Enterprise Library source code in other application blocks or applications that you build.


Enterprise Library includes documentation, a reference implementation, and source code. Enterprise Library embodies many design patterns, and demonstrates good architectural and coding techniques. You can use the library as a tool for learning architectural, design, and proven coding practices.

What’s in the Box?

  • New Blocks
    • Semantic Logging Application Block [video]
    • Transient Fault Handling Application Block (this application block was previously a part of the Enterprise Library Integration Pack for Windows Azure; in this release it has been generalized and updated to the latest technologies).
    • Updated Application Blocks – 6 blocks from previous versions have been updated:
      • Data Access Application Block
      • Exception Handling Application Block
      • Logging Application Block
      • Policy Injection Application Block
      • Validation Application Block
      • Unity Application Block/DI Container (v3.0)
  • New Programmatic Configuration – Streamlining programmatic configuration of all blocks and improving ease of learning and ease of experimentation.
  • Configuration Console – largely unchanged from the previous release.
  • Reference Implementation – To versions of the same application: one using Enterprise Library 5 and one using Enterprise Library 6 to illustrate the changes and to help users migrate.
  • Guides – The “Developer’s Guide to Enterprise Library” is designed to introduce users to the library and explain how to use it through short, practical code examples. The new “Dependency Injection with Unity” guide introduces users to the Dependency Injection pattern, describes the problems it can solve, and shows how to use the Unity container in their own applications.

How to Get It?

The primary shipping channel for code is NuGet. Search for Unity or EntLib inside NuGet Package Manager. Additionally, all deliverables are available as self-extractable zip packages via Microsoft Download Center. The guidance deliverables are available onCodeplex today. After post-production work is completed, they will be published on MSDN. You will also be able to order the hardcopies of the Developer’s Guides or get them in PDF, Epub, or Mobi formats.


NuGet must haves



 NuGet 사이트에서 유용한 정보를 리스트업 해서 보여주고 있다. 최근 인기 있는 프로젝트의 결과물의 리스트이다. 이곳에서 자신에게 필요한, 그리고 활발하게 개발 및 지원되고 있는 파일들을 받아서 실험해 보도록 하자.


NuGet Must Haves Link


[그림1] NuGet Must Haves site


C# Task의 TaskCreationOptions별 실행 비교와
 ThreadPool의 관계
(SetMinThreads SetMaxThreads)

http://msdn.microsoft.com/ko-kr/library/system.threading.threadpool.getmaxthreads.aspx



 이번 포스트에서는 "Thread를 컨트롤"의 라디오 버튼을 이용해서 SetMinThreads와 SetMaxThreads를 통해 제한된 상황을 만들어서 컨트롤 하는 시나리오를 검토해 보도록 하겠습니다. 지난 시간에 포스트 할때 배포 되었던 소스에서 약간더 수정한 소스를 다시 올려 드리며 추가된 화면 컨트롤에 대한 설명을 더 진행하도록 하겠다.


[그림1] 새로 추가된 테스트 화면


 "그림1"에서와 같이 추가된 컨트롤에 대한 설명을 "표1"에 설명 한다.


 컨트롤 명

 설명 

 Available Thread Count

 ThreadPool에서 사용 가능한 쓰레드 수 
 Available IOC Thread Count

 ThreadPool에서 I/O 관련 사용 가능한 쓰레드 수 

[표1] 추가된 컨트롤에 대한 설명


추가된 기능은 실시간으로 ThreadPool에서 사용가능한 쓰레드 수를 화면에 보여주도록 하여 Thread 컨트롤에서 어떻게 동작하는지 보다 정확히 알 수 있도록 하였다. 

 이제 "시나리오2"처럼 실행을 해보자.


시나리오2

 1.  "Thread를 컨트롤" 라디오 버튼을 클릭

 2. 기본 세팅되어 있는 Min Worker : 4, Max Worker : 4를 확인 한다.

 3. "Set" 버튼을 클릭

 4. "Async Processing" 버튼 클릭

 5. 동시 실행 갯수 확인

 6. MaxWorker을 6으로 수정

 7. "Clear" 버튼 클릭

 8. "Set" 버튼 클릭

 9. "Async Processing"버튼 클릭

10. 동시 실행 갯수 확인



이제 시나리오 2와 같이 실행하면 "Thread"컨트롤 라디오 버튼을 클릭하면 임의의 값으로 ThreadPool을 설정할 수 있으며 설정된 정보와 같이 Task 비 동기 실행이 되는 것을 확인 할 수 있다.


[그림1] MaxWorker을 4로 세팅하고 수행하는 화면



[그림2] MaxWorker을 6로 세팅하고 수행하는 화면


 "그림1"과 "그림2"에서 확인된 것과 같이 MaxWorker로 설정된 값을 기준으로 초기 동시 실행 갯수가 제한되는 것을 확인할 수 있다. 이제 대략적인 프로그램에 대한 설명을 마친다. 이제 전체적인 개념과 프로그램 화면에 대한 사용 방법 및 개념에 대해서 어느정도 이해를 할 수 있을 정도일 것으로 예상하고 이제 소스 코드에 대해 알아 보도록 하자. 다운받을 수 있게 소스 코드를 올려 놓을 것이니 포스트에서는 핵심 코드에 대해 부가적인 설명이 필요한 부분에 대해서만 진행 하겠다. 다시 한번 더 부탁드리자면 직접 디버깅을 통해 실행 하면서 체험해 보는것이 가장 좋고 소스에 같이 주석처리된 코멘트를 잘 읽어 간다면 보다 쉽게 체득할 수 있을거라 장담한다. 이제 코드를 살펴 보도록 하자.


다운받기

TaskTest_Next.zip

<RadioButton VerticalAlignment="Center" x:Name="rdoCustomerThreadControl" Content="Thread를 컨트롤" HorizontalAlignment="Center" Margin="5, 0, 0, 0"></RadioButton>
<StackPanel Orientation="Horizontal" Margin="20, 0, 0, 0" IsEnabled="{Binding ElementName=rdoCustomerThreadControl, Path=IsChecked, Mode=TwoWay}">

[코드1] 메인 화면의 Xaml중에서 Binding을 통해 값 연결


 "코드1"에서는 IsEnabled="{Binding ...}을 통해 radCustomerThreadControl의 값이 실시간으로 변홤에 따라서 IsEnabled의 값이 변경이 되도록 Xaml단에서 연결 시켜 놓는 작업이다. 이 작업은 UI단에서의 작업이므로 전체 로직에서 차지하는 비중을 차지 하지는 않는다. 다만 Xaml을 하는 장점을 한번 보여드리고자 하였다.


// 일정 간격마다 ThreadPool을 가져와서 화면에 보여준다.
Task.Factory.StartNew(() => {
    while (true)
    {
        //Min thread work
        int minWorkerminIOC;
        ThreadPool.GetMinThreads(out minWorkerout minIOC);
        // 가져온 값으로 화면에 보여준다.
        tbMinThreadCount.Dispatcher.BeginInvoke(new Action(() => { tbMinThreadCount.Text = minWorker.ToString(); }), null);
 
        //Max thread work
        int maxWorkermaxIOC;
        ThreadPool.GetMaxThreads(out maxWorkerout maxIOC);
        // 가져온 값으로 화면에 보여준다.
        tbMaxThreadCount.Dispatcher.BeginInvoke(new Action(() => { tbMaxThreadCount.Text = maxWorker.ToString(); }), null);
 
        // 특정 시간에 스레드 풀에 있는 실제 스레드 수를 확인
        int availableWorkThreadsavailableCompletionPortThreads;
        ThreadPool.GetAvailableThreads(out availableWorkThreadsout availableCompletionPortThreads);
 
        tbAvailableThreadCount.Dispatcher.BeginInvoke(new Action(() => {
            tbAvailableThreadCount.Text = availableWorkThreads.ToString();
            tbAvailableCompletionThreadCount.Text = availableCompletionPortThreads.ToString();
        }));
 
        Thread.Sleep(1000);
    }
});

[코드2] ThreadPool의 상태를 화면에 적용 하는 코드


 "코드2"는 ThreadPool의 상태를 가져와서 텍스트 박스에 할당하는 작업을 비 동기, 주기적으로 갱신하도록 하였다. 이 작업은 실행되는 풀의 내부 상태를 바로 알 수 있도록 하기 위한것이고 동시 실행갯수에 따라서 변경되는 것을 확인할 수 있다. 


// 스레드 작성 및 소멸을 관리하기 위한 알고리즘으로 전환하기 전에 새 요청에 따라 스레드 풀이 생성하는 스레드의 최소 수를 설정합니다.
// minWorker : 스레드 풀에서 필요할 때 만드는 작업자 I/O 스레드의 최소 개수입니다. 
// minIOC : 스레드 풀에서 필요할 때 만드는 비동기 I/O 스레드의 최소 개수입니다. File.BeginWrite(,,,) 와 같이 파일 관련 비동기 함수를 실해할 때 MinIOC를 쓰레드에서 실행한다. // http://msdn.microsoft.com/ko-kr/library/system.threading.threadpool.getmaxthreads.aspx 확인 가능
ThreadPool.SetMinThreads(minWorkerminIOC);
 
 
// 동시에 활성 상태가 될 수 있는 스레드 풀에 대한 요청 수를 설정합니다. 해당 수를 넘는 모든 요청은 스레드 풀 스레드가 사용 가능해질 때까지 큐에 대기 상태로 남아 있습니다.
// maxWorker : 스레드 풀에 있는 최대 작업자 스레드 수입니다. 
// minIOC : 스레드 풀에서 필요할 때 만드는 비동기 I/O 스레드의 최소 개수입니다. File.BeginWrite(,,,) 와 같이 파일 관련 비동기 함수를 실해할 때 MinIOC를 쓰레드에서 실행한다. // http://msdn.microsoft.com/ko-kr/library/system.threading.threadpool.getmaxthreads.aspx 확인 가능
ThreadPool.SetMaxThreads(maxWorkermaxIOC);

[코드3] ThreadPool에 Thread의 활동 제한을 설정 하는 코드


 "코드3"은 자동으로 변경되는 ThreadPool의 활성화 작업을 제한하는 코드로서 최저와 최고치를 설정하여 활성화 되는 쓰레드의 갯수를 제한한다. 이 작업은 "Thread를 컨트롤" 버튼을 눌러 활성화 시켰을 때에만 수행 하도록 되었다.


Task.Factory.StartNew(() =>
{
    for (int i = 1i <= counti++)
    {
        this.Dispatcher.BeginInvoke(new Action(() =>
        {
            var stackPanel = GetProgressBar();
            // 제공된 컨트롤을 리스트 객채에 넣는다.
            lstTaskResult.Items.Add(stackPanel);
        }));
    }
});

[코드4] 진행바를 ListItem 객체에 추가하는 코드


 GetProgressBar()에서 프로그래밍 적으로 프로그래스바를 화면에 보일 수 있도록 구성한 다음 진행바가 수행하는 작업을 연결 시켜 비 동기로 수행이 되도록 세팅된 StackPanel을 받아 ListItem 객체에 자식 컨트롤로 추가하는 코드다.


// Processing 버튼을 눌렀을 때 비 동기 Task가 활성화 되도록 처리 함.
Task.Factory.StartNew(() => {
 
    // Task를 활성화 시킴
    tasks.ForEach(task => task.Start());
 
    // 모두 완료가 될때까지 대기
    Task.WaitAll(tasks.ToArray());
 
    // 모든 Task가 수행을 마쳐 더 이상 관리가 필요 없어져서 관리에서 제거
    tasks.Clear();
});

[코드5] 진행바에 연결된 Task를 활성화 시키는 코드


 ForeEach의 Linq 구문을 통해 쉽게 수행이 되도록 하였으며 Task.WaitAll로 모두 완료가 될때가지 대기하도록 하였다. 완료가 되면 더 이상 사용하지 않는 Task이므로 Clear을 통해 모두 초기화 시켜주는 작업을 하였다. 


 이번 포스트는 소스코드를 포함하여 포스팅되었으며 전체 소스 설명 보다는 중요한 부분에 대해서 간략하게 설명하게 되었다. 자세한 설명은 소스를 받아 살펴 볼 수 있을 것이다.


 이 프로그램을 사용하여 Task의 비 동기 수행에 대한 특성을 좀더 쉽게 알 수 있었으면 하는 바램으로 이 글을 올리게 되었다.



Tip !

 아래와 같이 Framework에 따라 최대 허용 쓰레드 수가 다르다. ThreadPool.SetMaxThread를 통해 세팅할 수 있다.


 최대 쓰레드 수

 Platform 환경 

 1023

 32비트 .Net Framework 4

 32768  64비트 .Net Framework 4 
 250 per Core

 .Net Framework 3.5 

 25 per Core

 .Net Framework 2 




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

ing™       


C# Task의 TaskCreationOptions별 실행 비교와
 ThreadPool의 관계
(SetMinThreads SetMaxThreads)



 이번에는 Task가 내부적으로 어떤 방식으로 동작하고 수행되는지에 대해서 알아가는 시간을 가져 보고자 한다. 지금까지 흔히 Task.Factory.Start()를 통해서 타스크를 생성하고 비 동기로 수행이 되도록 작업을 수행하여 왔지만 깊이 있게 다뤄보지는 안았었다. 그래서 비주얼 적으로 확인하기 위해 WPF 프로젝트를 만들어 확인 할 것이다. 아래 "그림1"은 WPF를 통해 Task 수행결과를 확인할 수 있는 화면을 먼저 보도록 하자.


[그림1] WPF를 통해서 Task 수행 결과를 확인하는 화면


[이번에는 프로젝트를 압축해서 다운 받을 수 있게 올려 두었으니 바로 다운받아 확인할 수 있을 것이다.

TaskTest.zip (클릭하면 다운로드 됨)]


 우선 소스에 대한 설명 보다는 프로그램 화면을 확인하면서 설명하면 소스의 전체적인 흐름을 알 수 있을 것이고 그것을 알면 코드를 보다 쉽게 이해할 수 있으리라는 생각이다. 그럼 지금부터 잘 따라워 주면 좋겠다. 아래 "표1"은 버튼에 대한 설명 및 선택 인자에 대한 간단한 화면 설명부터 시작하도록 하겠다.


 Set

 ProgressBar를 ListItem에 추가하고 Task를 생성하여 추가된 Progress에 연결하여 비 동기로 진행바를 변경 할수 있도록 세팅한다.

 Clear

 추가된 컨트롤을 ListItem에서 모두 지운다.

 다른 변수에 대해서도 초기화를 수행 한다.

 Async Processing

 Set에서 추가된 컨트롤에 연결된 Task를 활성화 시켜 비 동기 수행 작업으로 진행바를 증가 시키도록 한다. 

 Async Cancel

 수행되고 있는 Task를 취소 한다  

 Min Thread Count   취소 동시 실행 갯수 
 Max Thread Count 

 최대 동시 실행 갯수 

 Thread를 컨트롤

 Radio 버튼으로 되어 있어서 한번 선택이 되면 취소할 수 없도록 하였다.
 선택되면 TextBox를 사용할 수 있도록 활성화 된다. 

 MinWorker  Min Thread Count를 해당 숫자로 세팅한다. 
 MinIOC  Min Thread IOC Count를 해당 숫자로 세팅한다.   
 MaxWorker

 Max Thread Count를 해당 숫자로 세팅한다. 

 MaxIOC

 Max Thread IOC Count를 해당 숫자로 세팅한다. 

 Slide Control

 Set 버튼을 눌렀을 때 한번에 추가될 컨트롤 갯수 선택 할 수 있음.

 ComboBox

 선택된 값으로 Task생성 시 TaskCreationOptions으로 세팅한다.


 None
 기본 동작이 사용되도록 지정합니다.
 PreferFairness
 가능한 한 공정한 방식, 즉 일찍 예약된 작업은 일찍 실행되고 나중에 예약된 작업은 나중에 실행될 수 있는 방식으로 작업을 예약하는 TaskScheduler에 대한 힌트입니다.
 LongRunning
 작업이 장기 실행되는 정교하지 않은 작업이 되도록 지정합니다.초과 구독을 보장할 수 있는 TaskScheduler에 대한 힌트를 제공합니다.
 AttachedToParent
 작업이 작업 계층 구조의 부모에 연결되도록 지정합니다.
 DenyChildAttach
 지정 하는 InvalidOperationException 만들어진된 작업에는 자식 작업을 첨부 하려고 시도 하는 경우에 throw 됩니다.
 HideScheduler
 앰비언트 스케줄러를 현재 스케줄러에서 만들어진된 작업으로 표시 되지 않습니다.즉, StartNew 또는 Continuewith와 같은 생성된 작업을 수행 하는 작업을 볼 수 있도록 Default 현재 스케줄러로 합니다.


[표1] 화면 행위 단위 설명표


 이제 표를 확인했으니 버튼을 누르면 어떻게 동작하는지 대충 알거라 생각한다. 그렇다면 이제 다운 받은 압축 파일을 풀어서 실행해 보자. 아무 버튼이나 눌러봐도 컴퓨터에 이상이 있지는 안을것이기에 걱정하지 말고 마구마구 눌러 보자. 만약 "Async Processing"을 눌렀다면 진행바가 움직이면서 작업이 수행되는 것을 확인할 수 있을 것이다. 몇번의 클릭 만으로도 전체적인 흐름을 간략하게나마 체험적으로 알 수 있을 것이다. 그렇다면 필자의 의도대로 아래와 같은 시나리오로 한번 따라서 실행해 보기를 바란다. 만약 제대로 따라 했다면 뭔가 이상한 점을 발견할 수 있을 것이다. 


시나리오 :
 1. "None" 상태의 콤보박스에서 "Set"버튼을 누르면 10개의 진행바가 추가되며(10개 추가됨) 
 2. "Async Processing"을 누르면 진행바가 움직인다. 작업이 완료 후
 3. "Clear" 버튼을 누르면 초기화가 되고 다시 한번 더
 4. "Set"을 누르고
 5. "Async Processing" 버튼을 누른다.
 

[시나리오1] Min, Max Thread 활성화 변경 확인 시나리오


 위와 같은 시나리오와 같은 방법으로 확인해 보면 처음에는 몇개의 진행바만(테스트 환경의 CPU Core마다 다를 수 있다, 4 Core면 네개의 진행바가 변경되는 것을 확인할 수 있을 것이다. 그렇지만 실제로는 3개의 진행바만 움직이는데 그 이유는 하나의 Task가 내부적으로 계속 활성화 되어 있어서 발생하는 현상이다. (활성화된 Task는 Min Thread Count와 Max Thread Count의 값을 지속적으로 화면에 갱신하여 변화되는 값을 보여주는 작업 진행하고 있다) 3개의 진행바가 변경이 되고 있는 상태에서 완료되지 않은 다른 진행바가 순차적으로 작업이 시작되어 값이 변경 된다. 이건 내부적으로 Thread.Sleep 때문에 일어나는 현상이기도 하다. 그러면서 최종적으로는 10개의 진행바가 모두 변경(내부적으로 Task가 실행이 되고 있다.)이 되는 것을 확인할 수 있다. 이와 같이 10개의 Task가 활성화 되어 있는 상태는 ThreadPool이 내부에 관리되고 있는 동시 실행 가능한 갯수를 10개로 세팅된 상태로 변경이 된다.  이런 상태에서 "시나리오1"의 3, 4, 5를 차례로 수행하면 첫번째 실행된 행동 패턴과는 다르게 10개의 진행바가 한번에 진행되는 것을 확인 할 수 있다.


 결과적으로 비 동기 Task 수행도 내부적으로는 ThreadPool에서 관리를 받으며 수행이 된다는 것을 확인 할 수 있다. 조금더 자세한 사항은 Task.Factory를 커스터마이징 하는 포스트에서도 확인할 수 있을 것이다. 


[그림2] PreferFaimess Async Processing 한번 실행 후 Clear -> Set -> Async Processing 실행 결과 화면


 "그림2"에서와 같이 동시 실행 갯수가 10개로 같이 시작 하는것을 확인 할 수 있다. 그렇지만 이런 유형은 Thread.Sleep(1)의 구문으로 인해 발생하는 현상이다. Sleep 없이 비 동기를 실행 하면 절대적인 CPU Core수의 제한을 받게 된다. 그에 대한 확인으로 Sleep대신에 Task.SpinWait(500000)으로 수정하고 비 동기로 실행하면 위와 같은 현상과는 다르게 나타난다.


 지금까지 전체적인 흐름과 하나의 시나리오에 대해서 설명을 하게 되었고 그 이외의 다른 시나리오와 소스에 대한 설명은 다음 포스트에 이어서 하도록 하겠다.


( 무엇보다 이번 포스트는 올려 놓은 소스를 다운 받아서 직접 테스트와 디버깅을 통해 몸소 체험과 분석을 통해 체득하는 과정이 필수라고 알려주고 싶다. 눈으로만 보는것과 직접 체험해 보는건 하늘과 땅 차이만큼 많은 깨우침의 차이를 가져올거라 믿고 있다. )


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

ing™       


C# 쓰레드의 지역 저장소 - ThreadLocal

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



 간혹 쓰레드마다 가지고 있어야 하는 별도의 공간이 필요할 때가 있을 것이다. 이때 사용할 수 있는 객체가 ThreadLocal 클래스다. .Net Framework 4에서 처음 등작 하였다. 이전에는 ThreadStaticAttribute이용해서 이와 같은 유사한 기능을 사용할 수 있었으나 이제는 ThreadLocal을 선언해 사용하기만 하면 된다. 아래 코드를 살펴 보자

#region ThreadLocal 테스트
[ThreadStatic]  // 쓰레드마다 다른 값을 가지도록 한다.
private static string ThreadValue = string.Empty;
 
/// <summary>
/// 데이터의 스레드 로컬 저장소를 제공하는 ThreadLocal를 테스트 한다.
/// </summary>
public void ThreadLocal_TestMethod()
{
    // 데이터의 스레드 로컬 저장소를 제공합니다.
    ThreadLocal<string> ThreadName = new ThreadLocal<string>(() =>
    {
        return "Thread" + Thread.CurrentThread.ManagedThreadId;
    });
 
    Action action = () =>
    {
        // 값이 true면 같은 쓰레드에서 이미 한번 실행되었다는 것이다.
        bool repeat = ThreadName.IsValueCreated;
 
        Console.WriteLine("ThreadName = {0} {1}"ThreadName.Valuerepeat ? "(repeat)" : "");
    };
 
    // Launch eight of them.  On 4 cores or less, you should see some repeat ThreadNames
    // 8개의 액션을 실행 하도록 한다.
    // 만약 동시 실행되는 Task가 4개라면 나머지 4개는 같은 쓰레드에서 ThreadLocal을 접근 했다는 것을 뜻한다.
    // 같은 쓰레드 안에서 사용할 값을 컨트롤 할 수 있다.
    Parallel.Invoke(actionactionactionactionactionactionactionaction);
 
    // 만약 쓰레드 마다 다른 값을 가져가기 위해서는 ThreadStaticAttribute로 선언해서 사용하면 된다.
    // [ThreadStatic] ThreadValue 변수 처럼 말이다.
 
    // Dispose when you are done
    ThreadName.Dispose();
}
#endregion

[코드1] ThreadLocal<T> 코드 테스트


 위 코드를 실행한 결과 화면인 "그림1"을 보면 어떻게 실행되는지 쉽게 알 수 있을 것이다.


[그림1] ThreadLocal 결과 화면


 

소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다.. 실제 개발에서도 필요한 소스는 단순히 Copy & Paste 만으로도 사용할 수 있습니다. 그리고 주석을 이용해 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™       


+ Recent posts