삽질의 현장/- .NET

#080_닷넷(.NET)_.Net Framework 기본 - Thread Pool &TPL

shovelman 2015. 11. 15. 22:14


안녕하세요 삽잡이입니다.


쓰레드 풀은 간단하게, 쓰레드의 모임 공간입니다.


어떤 작업이 무수히 많이 들어올 때

각각 작업을 모두 쓰레드 하나씩 독립적으로 수행한다고 해봅시다.

들어오는 작업마다 쓰레드를 띄워주면 얼마나 비효율적이겠습니까?


무조건 쓰레드를 띄워버리면 배보다 배꼽이 더 커지게 되버리죠.

왜냐하면, 쓰레드를 관리하는 리소스 뿐만 아니라, 

스케쥴링 비용까지 들기 때문에 무조건 쓰레드를 띄우지 않는게 좋다 이겁니다.

따라서, 쓰레드 풀이라는 것을 만들게 된 것입니다.




예를 들어,

쓰레드를 3개만 만들어 두고 풀에 집어넣었다가,

작업이 들어오게되면 풀에 있던 쓰레드가 처리를 하게 만드는 것입니다.


즉, 쓰레드를 3개까지만 만들어두는 것입니다.

작업이 끝나게 되면 풀에 들어가고,

또 작업이 오면 수행하고 이렇게 말입니다.


쉽게 생각하시면,

풀은 쓰레드가 놀면서 쉬는 공간이라고 생각할 수 있겠군요.


풀에서 쓰레드가 놀고있다가

작업들이 들어오게 되면 수생할 수 있도록 매핑 시켜주는 것입니다.


우선, 대리자를 통해 

쓰레드가 실행하고자하는 메서드 기능을 지정해줄 수 있습니다.



참고로, 모든 delegate는 callback입니다! 



WaitCallback이라는 대리자는

하나의 object 매개 변수를 받고, 반환값이 void인 메서드를 가리킬 수 있습니다.

Wrapper 메서드 즉, 감싸는 메서드를 만들었다고 생각하시면 됩니다.


어찌됬건, state 객체를 하나 설정해주고,

쓰레드보러 메서드를 수행하도록 할 수 있다 이겁니다.



쓰레드 풀로 돌리려고 하면, 

무조건 WaitCallback 딜리게이트가 정해진 형식의 메서드를 가리켜야하는데, 

이 형식이 return 타입이 void이고, 매개변수로 object형을 받아야된다 이겁니다.

즉, 쓰레드 풀을 돌리려면 delegate가 callback 함수를 가리켜야하는데

인수가 이와 같이 정해져있다 이겁니다.


이렇게 등록을 해주면,

기존에 Primary Thread가 혼자 처리하던 작업이 아닌,

독립적으로 Thread 객체를 만들어 작업을 수행하던 것이 아닌,

Thread Pool을 통해 작업을 돌릴 수 있게 됩니다.


따라서 내가 Thread Pool에 요청하고자하는 메서드가

해당 형식이 아니라면 Wrapping 해서 호출하자 이거지요.


간단하게 쓰레드 풀에 담겨져있는 쓰레드가 

자동적으로 어떤 기능을 호출하도록

작업의 양을 나타내기 위해 정형화 된 형식을 만든 것입니다.

이 정형화 된 작업 형식이 하고자하는 작업의 양을 뜻하는 것이됩니다.


따라서, 직접 호출하지 않고

WaitCallback이라는 작업의 양을 만들어준 것입니다.



정적 메서드를 돌림으로써 

callback이라는 작업의 양(task)과, 

함수를 호출했을때의 object 전달 인수인 객체를 인수로 넘깁니다.


이를 통해 작업을 큐에 넣어주면,

OS에서 적정하게 자동적으로 쓰래드의 개수를 할당해주어서 풀에 담기고 

작업을 나눠 쓰레드를 동작시키게 됩니다.


아무튼...

적정한 개수의 쓰레드를 제공해주고 작업을 담기만 하면 

Thread Pool 에 있던 쓰레드들이 알아서 작업을 처리해주게 됩니다.


그런데, 이보다 더 좋은 기능이 있습니다...

바로, TPL(Task Parallel Library)이라는 작업 병렬 라이브러리를 말하지요...


TPL 라이브러리의 주요 클래스인 Parallel 클래스의 장점은

알아서 쓰레드 풀에서 쓰레드를 꺼내주며 동시성을 관리해줍니다.

즉, 사용자가 특별히 할 일 없이 

적정한 쓰레드의 개수를 유지할 수 있게 쓰레드 풀이라는 개념을 사용했는데

TPL 이라는 라이브러리가 쓰레드 풀까지 관리해준다 이겁니다.

따라서, 굳이 쓰레드 풀까지 사용할 필요가 없어졌지요...


그리고 TPL 은 LINQ까지 가능합니다.

PLINQ라고, 병렬처리에 LINQ를 던질 수 있다는 것입니다.

LINQ는 우리가 이전까지 굉장히 많은 데이터를 대상으로 했는데,

이제 굉장히 많은 작업을 대상으로 LINQ를 동작시킬 수 있다 이거지요.


Parallel 클래스 자체가 많은 작업을 대상을, 

시간이 많이 걸리는 작업을 빠르게 작업을 하고자 하는 것입니다.



심지어 속도차이가 거의 3분의 1!? 



모든 작업들이 독립적으로 굉장히 빨리 이루어진죠...

따라서, 코어의 수가 늘어날 때마다 작업의 양이 엄청나게 달라집니다.

즉, TPL이 어마어마한 강점을 발휘하게 되는 것입니다.


TPL을 장착하고 있는 언어가 많지 않은데

C#, 닷넷 쪽은 가지고 있는데 이 어마어마한 기능을 장착하고 있는 것이지요.


물론, 쓰레드 풀 만들고 돌려줘도 되고,

각각의 쓰레드를 한번에 띄워서 작업을 수행해도 되지만,

TPL이 가장 효율적이고 빠르다는 것을 알 수 있습니다.


쓰레드를 띄우기도 참 쉽습니다.



Primary 쓰레드는 User와 대화하는 일을 해야하기 때문에,

부가적인 일을 쓰레드를 통해 처리하는 것입니다.


쓰레드를 띄우는 방법에는 크게 두가지를 배웠었죠.

비동기 delegate를 쓰는 방법과,

실제 쓰레드를 생성해서 띄우는 방법 말입니다.


그런데, 이제는 위의 예제 코드와 같이

TPL 라이브러리를 만들면서 Task를 통해 사용하는 것이 더 쉽다 이거지요.



이처럼 람다식을 쓸 수 있습니다.

즉, 어떤 함수던지 쓰레드로 돌릴 수 있다는 것입니다.

람다식을 사용해서 간단하게 표기를 하면 쓰레드로 수행된다 이겁니다.



foreach 문을 사용하나 

Paralle에서 제공하는 ForEach 메서드를 사용하나 동작은 똑같습니다.


하지만, 전자는 단일처리를 하는 것이고,

Paralle을 사용하면, 병렬처리를 하게 되는 것입니다.

병렬 처리를 사용하는 방법은 간단합니다.

그런데, 반복적으로 처리하고 많은 시간을 요하게 된다면,

그런 작업을 병렬처리 작업을 사용한다 이겁니다.


그런데, Paralle의 ForEach를 사용하게 되면 병렬 처리를 쓸 수 있는데,

이 놈은 IEnumerable 형식의 객체만 사용이 가능합니다.

즉, 배열 객체를 말하는 것이지요. foreach와 똑같습니다.


그리고, 하나씩 원소를 가져와서 람다식의 행동을 하게 되는 것입니다.

이를 통해 정정한 개수의 쓰레드를 가져와서 

적정한 처리를 자동적으로 수행하는 것이죠.



Paralle을 사용하며 가장 핵심적인 기능중 하나가

바로, '취소 요청'을 처리하는 것입니다.

즉, 작업을 취소시키는 것이지요.


작업을 취소 시키기 위해서 쓰레드가 하던 작업을 마저 끝내고, 

쓰레드가 수행되지 않는 것들을 모두 종료 시킵니다.

또한, 쓰레드 풀에 있는 Paralle에 관련된 모든 리소스를 마무리 작업시킨다 이겁니다.

즉, 리소스 해제를 해주는 것이지요.



그래서 취소를 하기 위한 개념이 필요합니다.

정의에 의해서 동작하게 되는데,

cancel.Token이라고 해서 CancellationTokenSource라는 Event 객체를 만듭니다.

즉, 병렬처리를 취소 처리할 수 있는 객체를 만드는 것입니다.


그런데, 객체를 만들고 cancle만 호출시킨다고 Paralle과 연결이 되는 것이 아닙니다.

전혀 연관성이 없다 이겁니다.



따라서, Paralle이 취소 동작을 인식하기 위해서,

즉, 쓰레드가 취소 이벤트를 받기 위해서 작업 안에 취소에 대한 내용이 들어있어야합니다.


이벤트를 받게되면, 취소 요청을 받아들이라는 것을 알리는 것이지요.

그런데, 이는 하고자하는 행동에 지나지 않습니다.

따라서, 서로간에 Paralle과 연결되기 위한 다리역할을 하는 메커니즘이 필요합니다.



ForEach문은 옵션을 가질 수 있습니다.

즉, 두번째 인수로, Paralle에 대한 옵션을 넣을 수 있다 이겁니다.

그리고, 그 옵션을 넣어두면 동작을 수행할 때 옵션에 참조를 얻을 수 있습니다.


그런데, 만약 옵션의 참조가 CancllationToken을 갖고 있으면

해당 이벤트를 ForEach문 안에서 받을 수 있게 됩니다.

따라서, 옵션은 cancllatationToken이 

종료 처리를 하기 위한 옵션이라는 설정이 있어야합니다.



이처럼 객체를 토큰에 연결해주고 그 옵션을 ForEach에 달아주면,

cancellationToken이 CancelToken에 대한 참조이기 때문에 

멈추게 할 수 있게 된다 이겁니다.


이것이 Parallel의 처리입니다.

취소를 하고 난 뒤에는 분명 예외가 발생할 것인데,

따라서 catch문장을 통해 예외를 잡고 예외를 출력할 수 있습니다.


간단하게 Parallel은 PLINQ를 사용할 수 있습니다.

쉬우니까 많이 사용하지요...



이때에 핵심은, LINQ에서 PLINQ로 변환해주는 확장메서드인 AsParallel() 입니다.


다른 것은 없고,

이전에는 하나의 쓰레드가 담당했다면,

이와 같은 방식으로 여러개의 쓰레드가 동시에 작업을 참여할 수 있데 된다 이겁니다.


PLINQ에서도 역시 취소시킬때에는 취소 동작을 연결시킵니다.


병렬처리를 하는 작업은 

오래... 그리고 많이 작업을 처리하다보니까,

언제든 사용자가 취소하고 싶을 때 취소할 수 있도록 요구사항을 만들어내야합니다.


따라서 취소 작업이 중요하다고 하는 것이지요.


이번시간은 여기까지 하도록 하겠습니다.

이상 삽잡이였습니다!