일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- SSMS
- SQLServer
- async await
- ThreadPool
- 느린 저장프로시저
- validation
- 쿼리 최적화
- 영어공부
- SQL Server Optimizer
- TPL
- slow in the application
- IdentityServer4
- .net
- 실행계획 원리
- query
- execution plan
- identityserver3
- await
- esl
- oauth2
- Dataannotation
- 저장프로시저
- fast in ssms
- identityserver
- stored procedure
- english
- task
- MSSQL
- C#
- async
- Today
- Total
Genius DM
C# > ThreadPool in details 본문
ThreadPool 에 대해서
빨리 Task 에 대해서 정리하고 싶지만, 그 근간인 ThreadPool 을 먼저 정리하는 것이 순서이기에 ThreadPool 에 대해서 한번 더 다뤄보려고 한다. 이번엔 이전 포스트보다 좀 더 디테일에 집중한다.
ThreadPool 은 두 종류의 쓰레드를 지닌다.
- WorkerThread
Main Thread 를 제외한 모든 Thread 를 의미한다. 이 Worker (Thread) 에 할당되는 Work 는 Thread 에 할당되는 delegate 를 의미하며, 일련의 작업 모두를 지칭한다. - CompletionPortThread
I/O Thread 로 불리며 .NET 에서 ThreadPool 이 NativeOverlapped Callback 을 보내기 위해 보관하는 Thread 를 의미한다.
일단 이 두 Thread 는 일반적인 Thread 이다. 하지만 ThreadPool 에서 이 둘을 논리적으로 분리하여 정의짓고 따로 보관하며 관리하는 이유는 데드락을 방지하기 위한 것이다. ThreadPool 에서 너무나 많은 작업이 Queue 에 적재되고, 이를 빠르게 소비하기 위해서 WorkerThread 가 Native I/O Callback 을 발생시켜야 하는 Thread 에 까지 작업을 할당해버렸다고 가정해보자. I/O 완료 Callback 을 전달한 Thread 가 없으니, I/O 요청이 완료되지 않아 해당 자원을 선점한 상태로 남는 Thread 가 금새 생길 것이고, 결국 데드락이 발생하게 될 것이다.
- NativeOverlapped Callback
일종의 Struct 이며, overlapped 된 win32 호출을 의미한다. win32 호출이란 user32.dll 이나 kernel32.dll 같이 PInvoke 를 통해서 호출할 수 있는 Windows API 를 의미한다.
아래 그림을 보면 CompletionPortThread 가 왜 필요한지 어렴풋이 짐작이 가능해질 것이다. Stephen Cleary 의 There is no thread 에서 코드에서 작성한 Thread 가 어떻게 디바이스 레벨까지 내려가는지 자세히 설명하고 있으니 참고하면 좋겠다. 이 그림에서 의미하는 Overlapped 가 바로 NativeOverlapped Callback 전달 시점이다.
ThreadPool 의 알고리즘
- Starvation
컴퓨터 사이언스에서 Starvation 은 동시 처리에서 필요한 작업 요청이 지속적으로 거절당하여 처리되어야 하는 작업을 수행할 수 없게되는 경우를 일컫는다. 의도적인 DoS 공격으로 인해 Starvation 이 발생할 수 있는데, 그것이 좋은 예인 것 같다. WorkerThread 에 작업이 모두 할당되어 있고, Queue 에 지나치게 많은 작업이 쌓여나가고 있는 상황이되면, 배고픈 상태 (Starved) 로 Queue 에서 대기 중인 작업을 처리하기 위해 새로운 Thread 를 추가하는 메커니즘이 ThreadPool 에 구현되어 있다. - Hill climbing heuristic
Heuristic 이란 find / discover 를 의미하며 스스로 찾는 방식을 의미한다. Hill Climbing 은 알고리즘 중에 하나인데, 문제를 해결하기 위해 다양한 방식을 고안해내고 점진적으로 보다 나은 해결책을 찾아나가는 방식의 알고리즘이다. 이 알고리즘이 ThreadPool 에 적용되었다.
Thread 가 I/O 에 의해 블럭되거나 프로세스를 점유하는 대기 상태에 있는 경우 CPU 코어를 효율적으로 활용하기 위해 동작한다. 가장 특징적으로 ThreadPool 에서 작업이 끝났을 경우, 그리고 500ms 의 간격마다 새로운 Thread 를 생성할 기회를 갖게된다. 이 때 최근 발생한 Thread 개수의 변화를 참고하게 된다. ThreadPool 이 Thread 를 늘리는 것이 옳다고 판단하면, 새로운 Thread 가 추가 될 것이고, 그렇지 않으면 Thread 를 삭제해서 ThreadPool.GetMinThreads 에서 얻을 수 있는 최소 유지 Thread 값으로 돌아가려고 할 것이다. 이렇게 상황에 따라 능동적으로 대응하는 알고리즘을 Hill climbing heuristic 이라고 부른다.
알고리즘은 집어 치우고, ThreadPool 에서 항상 많은 Thread 를 보유하면 되지 않을까?
- 더 많은 Thread 는 더 많은 Context-switching 을 의미한다. 그리고 이 Context-switching 은 CPU 오버헤드를 유발한다. 많은 수의 Thread 가 존재할 때, 이러한 오버헤드는 매우 큰 영향을 미치게 된다.
- 더 많은 Thread 는 더 많은 메모리 스택을 보유하게 됨을 의미하며 이는 데이터의 로컬리티를 저해한다. 마치 저글링을 하듯 CPU 에서 각 Thread 마다 메모리 스택을 보유하고 이를 캐싱하게 되면 CPU 캐싱의 효율이 급격하게 저하된다.
논리적 프로세스 수 보다 Thread 를 많이 보유함으로써 얻을 수 있는 것은 CPU 를 항상 바쁘게 만들어 몇몇 Thread 가 블럭된 상태에서도 다른 여분의 Thread 에 작업을 할당하여 많은 작업을 처리할 수 있다는 데 있다. 하지만 블럭 상태에 대해 너무 민감하게 반응하지 않도록 주의해야한다.
결론적으로 이러한 시나리오까지 고려하고 고안한 것이 Hill climbing heruistic 이기 때문에, 더 나은 제안을 할 수 없다면 더 이상 의문을 품지 않는 것이 좋겠다. 참고로 이 ThreadPool 디자인과 구현에 대한 논문도 존재한다. ( 3 명의 마이크로소프트 소속으로 연구에 참여를 했는데, 그 중 하나는 이제 Google 소속인 것 같다. )
ThreadPool 의 과거
Faster FIFO
Task Parallel Library 등장
ThreadPool in details.
I'd like to write a post for Task, but first things first, I'm going to talk about the thread pool since it is the underlying mechanism of the task. I already put up a post for the basic of the thread pool in the last post, I'm going to look at the details this time.
ThreadPool has two types of threads.
- WorkerThread
This means pretty much every threads except for the main thread. The work that the thread pool offloads is typically a delegate and it means any jobs in your code. - CompletionPortThread
It's called I/O Thread and Threadpool in .NET separately maintains this thread to dispatch NativeOverlapped Callback.
First of all, these two threads are technically just normal threads. The reason the thread pool logically splits those into two types and maintains separately is to avoid deadlock. Imagine that there are too many tasks in the queue and the thread pool is offloading the works to the worker threads to consume the waiting tasks faster, ended up allocating jobs to the threads for the native I/O callback. There's no thread reserved for dispatching the I/O completion callback and it directly leads to the occupation of I/O resource for a long period, and eventually causes deadlock.
- NativeOverlapped Callback
Kind of struct, and it means the overlapped win32 call. What it means by win32 call is the windows APIs in user32.dll or kernel32.dll that could be invoked with PInvoke in managed code.
If you learn a little bit about the diagram stacks below, you may see the point in CompletionPortThread. I recommend you to read There is no thread by Stephen Cleary, which explains how a thread in your code will be interacting with your devices in a brief detail. The Overlapped yellow square is the exact spot for NativeOverlapped callback.
Algorithm in ThreadPool
- Starvation
In computer science, Starvation means a case of concurrent computing in which a process is repeatedly denied the required resources to process a job. An intentional DoS attack could result in starvation, I think that's the good case. If demanding tasks are offloaded to all of worker threads in the thread pool and the queue is piling up the tasks, there would be a plenty of starved items in the thread pool queue. To deal with this, the thread pool has a mechanism to add a new thread for the waiting task in its queue. - Hill climbing heuristic
Heuristic means "find / discover" and more accurately, it's a process or method that actively and responsively find a better way to solve a problem for itself. Hill Climbing is one of algorithms and It is an iterative algorithm that starts with an arbitrary solution to a problem, then attempts to find a better solution by making an incremental change to the solution. If the change produces a better solution, another incremental change is made to the new solution, and so on until no further improvements can be found. This is the behind scene in the thread pool.
This algorithm works entirely for effectively utilizing the CPU cores when threads are blocked by I/O or in waiting state by the process occupation. It makes a decision itself to add a new thread to its pool every time one of the threads finished its job or the static 500ms interval. When the thread pool thinks it'd better to create another thread, then one will be joining the forces otherwise will be falling back untill it reaches the minimum number of threads, which you can get from ThreadPool.GetMinThreads API. This responsive technique is called Hill climbing heuristic.
Forget about the algorithm. What about having a lot of threads in the pool?
- The more threads you have, the more context-switching there would be. This can lead to CPU overhead and if you have a lot of threads, the overhead could pose a significant impact on the system.
- The more threads you have, the more memory stacks should be created, and this can impact the data locality. If CPU has a memory stack per a thread and caches them like it juggles them all, then the caching would be very less effective.
Holding more threads than the number of the logical processors has a limited benefit. It just makes CPU always busy and do more works by offloading some work items to available threads when there're some of blocked, unavailable threads. But we should try not to overreact to the block state.
To put it in a nutshell, the hill climbing heuristic takes care of this kind of scenario as well, so if you cannot come up with the better, breaking-through solutions, accepting it as it is might be good for your mental health. FYI, there's a paper for the thread pool design and implementation. ( It's published by 3 Microsoft employees, but it looks like one of them is working for Google right now. )
ThreadPool in the past.
Faster FIFO
Introduction of Task Parallel Library
'.NET' 카테고리의 다른 글
.NET > Task Basics (0) | 2018.09.15 |
---|---|
C# > ThreadPool Basic (0) | 2018.08.20 |
Image processing > Add a bitmap header to an image buffer. (0) | 2018.08.14 |
IdentityServer > Allow specific users in a client. (0) | 2018.08.11 |
IIS > IIS Express & IIS applicationhost.config path (0) | 2018.08.06 |