Genius DM

.NET > Task Basics 본문

.NET

.NET > Task Basics

Damon Jung 2018. 9. 15. 04:33


Task 기본


요즘은 대부분의 컴퓨터 및 전자 기기가 강력한 멀티 코어를 갖추고 나온다. 컴퓨터 프로그래밍 세계에서 이는 곧 병렬처리가 중요해진다는 것을 의미한다. 컴퓨터 시스템에서 단 하나의 프로세스만을 사용하는 경우는 이제 DOS 만큼 머나먼 과거에나 존재하는 이야기가 되었다. 가장 세련되고 가장 현대적인 프로그래밍 언어 중 하나인 C# 에서 이러한 병렬 프로그래밍 기능을 .NET Framework 4 부터 탑재시켰다. 더 정확하게 얘기하면 .NET Framework 가 이를 지원한다. 이 새로운 프레임워크 버전에서 TPL ( Task Parallel Library ) 이 추가되었는데, 병렬성을 지원하기 위한 자료 구조 다수와 작업 및 작업 스케쥴링을 위한 알고리즘이 탑재되어 있다. TPL 에서 OS 수준의 스케쥴링을 흉내내려는 것 처럼 보일 정도인데, 이 이야기를 리눅스 OS 내부의 스케쥴러를 Task 와 비교하면서 추후에 자세하게 다뤄볼 예정이다. 




.NET 병렬 프로그래밍 아키텍처


쓰레드는 위 다이어 그램의 가장 하부에 존재한다. TPL 과 기타 다른 요소들은 큰 그림에서 볼 때 모두 쓰레드 보다 추상화된 상위 단계에서 구현된 것들이다.


C# 에서 프로젝트에서 코딩을 하고 컴파일을 하게되면 C# 컴파일러가 해당 코드를 컴파일하고 IL ( Intermediate Language ) 로 변경하게 된다. 이 과정에서 async await 같은 병렬 코드가 병렬 수행을 위해 상당히 다른 모습으로 변하게 되는데, 아래 예제는 IL 코드 수준에서 async await 코드가 어떻게 변화되는지를 보여준다. 코딩에 익숙한 개발자라면 async await, Task " 아 이래서 await 가 그렇게 동작할 수 있구나 !! " 라고 어렴풋이나마 이해할 수 있을 것이다.



위 다이어그램이나 IL 코드는 기본을 다루는 것 치고는 꽤 깊이 들어갔다. 빨리 정리하고 다음으로 넘어가도록 하자.




TPL ?

TPL 은 Task Parallel Library 의 약자이고, 라이브러리 답게 여러 API 와 타입들을 제공하며, System.Threading 과 System.Threading.Tasks 네임스페이스 하에 정의되어 있다. 병렬성과 동시성을 코드에 추가하기 쉽게 해주는 라이브러리이다. TPL 은 쓰레드의 추상화이며, 특히 쓰레드 풀에 대한 추상화라고 볼 수 있다. 이에 따라 상태 관리나 작업 취소 등의 로우레벨 수준의 작업을 지원하여 쓰레드 풀의 행위를 쉽게 조작할 수 있게 해준다. TPL 이전에는 병렬성을 위해 쓰레드와 락을 로우레벨에서 조작해야 했었는데, 이제 TPL 을 통해서 쉽게 할 수 있다.





Thread, ThreadPool, Task 차이점

나를 포함한 많은 개발자들이 병렬처리를 떠올리면서 이 세 가지의 차이점에 대해 혼란스러워 한다. 각 요소들에 대한 간략한 설명을 보자.



Thread

실제 OS 수준의 쓰레드를 나타내준다. 고유의 스택과 커널 리소스를 포함한다. Abort(), Suspend(), Resume() 등의 작업을 C# 쓰레드 오브젝트를 통해서 할 수 있다.


ThreadPool

CLR 에서 관리하는 쓰레드들을 묶어놓은 것이다. 개발자는 쓰레드 풀을 작업 할당이나 풀의 사이즈를 조정하는 것 외에는 기본적으로 아무것도 할 수 없다. QueueUserWorkItem 은 쓰레드 풀을 사용해봤다면 한 번쯤은 들어봤을 것이다. 이것을 통해서 작업을 쓰레드 풀에 할당한다.

쓰레드 풀의 장점은 병렬처리시 지나치게 많은 쓰레드를 생성하는 것에 대한 오버헤드를 방지할 수 있다는 것이다. 단점으로는 할당한 작업이 완료가 되었는지 직접적으로 알 수 없다는 데 있다. 또 쓰레드 풀은 호출하고 끝내는 형식의 작업이나 실행 후에 아무런 신경을 쓸 필요 없는 작업에 적합하다. 짧고 빠르게 끝나는 처리가 아니라, 길고 오래 걸리는 작업이라면 쓰레드 풀 보다는 쓰레드를 직접 생성하여 사용하는게 성능에 더 좋을 수 있다.


Task

TPL 의 클래스이다. 쓰레드 풀 처럼 Task 는 OS 쓰레드를 생성하지 않는다. Task 스케쥴러를 통해 실행되며, 이 스케쥴러는 쓰레드 풀을 사용한다. 그러나 Task 와 쓰레드 풀에는 뚜렷한 차이점이 존재한다. 쓰레드 풀을 사용할 때는 할 수 있는 것이 별로 없다. 작업을 쓰레드 풀의 큐에 할당한 후에는 무슨 일이 일어나는지 알 수 없다.

그러나 Task 는 강력한 API 와 상태 관리를 지원해준다. 작업을 취소할 수 있고, 작업이 끝날 때까지 대기할 수 있고, 작업이 완료되자마자 특정한 후 처리를 곧 바로 수행할 수 있게 해준다. 리턴 타입을 지정해서 작업에 대한 결과를 받을 수 있다는 것 또한 강력한 기능 중 하나이다. Task 스케쥴러가 쓰레드 풀 상에서 동작한다고 언급했고, 쓰레드 풀에서는 작고 가벼운 단위의 작업이 적합하다고도 했고, 오래 걸리는 작업은 적합하지 않다고 언급하였다. Task 는 이러한 제한사항에 대한 고려를 하여 Task 생성자 옵션에 LongRunning 이라는 옵션을 제공하여 길고 오래 걸리는 작업 또한 Task 스케쥴러에서 새로운 Thread 생성 여부를 판단하게 하였다. LongRunning 이라면 ThreadPool 의 기존 Thread 를 활용하는 것 보다는 새로운 Thread 를 생성하는 것이 유리하기 때문에, LongRunning 옵션 지정시 Task 스케쥴러가 ThreadPool 에 새로운 Thread 를 생성하고 해당 작업을 할당할 확률이 상당히 높아진다.







마치며

.NET 4 에서 Thread pool 을 개선하기 위해 마이크로소프트 담당 팀에서 Task 에 대한 언급을 하면서 한 말이 있는데, 이 한마디로 사실 Task 를 정의 할 수 있다.

Task is now the preferred way to queue work to the thread pool.
Task 는 이제 Thread pool 에 작업을 할당하기 위한 기본 방식이 되었습니다.


그렇다고 QueueUserWorkItem 이 완벽히 Outdated 되었는가? 에 대해서는 곰곰히 생각해봐야 할 것 같다. 다음 포스트에서는 Thread 가 얼마나 무거운 리소스인지, 왜 ThreadPool 를 직접 사용하기 보다는 Task 를 사용하는 것이 좋은지, Task 의 종류는 무엇이 있는지 등 Task 의 디테일을 다뤄볼 것이다.

















Task Basics


Majority of computers, devices, and gadgets are out there equipped with powerful multi cores these days. This means that the parallelism is getting important in computer programming world. Using only one process in a computer system is now far behind in the past. ( say DOS. ) As the fanciest and probably the most modern programming language out there, C#, more precisely, .NET framework support for parallel programming features since .NET framework 4. The new fourth version provides TPL ( Task Parallel Library ), which has a several data structures for properly backing up parallelism and a set of algorithms for tasks and task schedulling. It seems like the task in TPL is trying to mimic the scheduler in OS itself. I'm going to talk about the details later, comparing the linux scheduler with task in .NET in the near future. 




Overall architecture of parallel programming in .NET framework


Thread resides far behind in the underneath of the diagram. TPL and others are just on top of the threads in a overall picture.


When you finished tweaking codes and compiled the project, C# compiler will compile your code and changes it to IL. In this context, your parallel codes such as async-await will be turning into quite different things for parallelizing your jobs. Here's a brief changed shape for an async await code in IL level. If you are familiar with codes, you're going to get a slight idea of what's the behind scene for async await and Task, exclamating " Oh that's why ! "




This diagram and IL code would be a little bit overwhelming for a basic article. So let's wrap this up quickly.




TPL ?

TPL, Task Parallel Library, as a library, provides a set of APIs and types in System.Threading and System.Threading.Tasks namespaces. It simplifies adding parallelism and concurrency to your code. It is also an abstraction of thread, especially the thread pool, so that you can easily manipulate the behavior by supporting the state management, cancellation support, and other low level features. In the early days of parallelism, you had to manipulate threads and locks at lower level. You can achieve that by TPL now.





Difference between Thread, ThreadPool, Task

Many developers including me are confused with these three things when thinking about parallelism. I'm going to give a brief explanation for each items.



Thread

Represents an actual OS level thread, which has its own stack and kernel resources. You can Abort(), Suspend(), Resume() through the thread object in C#.


ThreadPool

It's a wrapper around a pool of threads maintained by CLR. The thread pool doesn't give you any control at all, except for setting the size of the pool or offload a work by the famous API you must have heard of at least once, QueueUserWorkItem.

The advantage of using it is that you can avoid the overhead of creating too many threads. The disadvantage would be that there's no way to find out when a work item is done or not. Using thread pool is ideal for a fire and go job and something that you shouldn't care about after executing it. If your job requires a long, dedicated work, then using thread pool could be a bad decision.


Task

It's from TPL. Just like the thread pool, a task doesn't create its own OS thread. It's executed by a task scheduler, the default scheduler that simply runs on the thread pool. But there's a significant difference between the task and the thread pool. When you use thread pool, there's pretty much nothing you can do about it. You assign a job to the thread pool's queue and you never know what's going on underneath there.

But the task provides more powerful APIs and state management for you. You can cancel a work item, wait for it, and continue another job as soon as it's done. Returning a result is also a powerful feature as well. I mentioned earlier that the task scheduler runs on the thread pool. And I also said a small, lightweight unit of work is ideal for thread pool and long-running would be not. Task is well aware of this limitation in thread pool and provides a construct option for itself, it's a LongRunning option, which may result in creating a new thread on the thread pool.




Conclusion

The task force in Microsoft mentioned this below once, when they released the TPL in .NET 4.0 framework. Well actually, this one quote explains what Task is about.

Task is now the preferred way to queue work to the thread pool.


Well, obviously the task became seemingly the standard way to use the thread pool I guess. But is using the thread pool by calling the QueueUserWorkItem completely outdated? well I think we need to give more thoughts on that subject. In the next post, I'm going to write the details of the Task such as how heavy the thread actually is, why Task became a preferred way over the thread pool, what types of task exist in TPL, and etc.




















Comments