글 작성자: Sowhat_93

스레드가 마지막으로 사용했던 레지스터 정보인 Context를 로드한다.

할당받은 시간을 모두 사용하고 다시 메모리에 Context를 저장시킨다.

(상술 했듯이 커널메모리 안의 스레드 커널 오브젝트 안에 Context가 저장된다.)

이게 반복되면 시스템에 돌고있는 스레드들이 모두 실행되는 것이다.

 

윈도우즈 OS는 실시간 OS가 아닌 선점형 멀티스레드 기반 OS이다.

OS에 의해 물흐르듯 일어나는 스케쥴링 작업을

App을 작성하는 프로그래머의 마음대로 변경하거나 교체 하는 것에는 한계가 있다.

App에서의 스레드 통제는 어디까지나 OS의 판단하에서 적합한 기준내에서 실행된다.

 

 

스레드의 정지와 재개

스레드의 정지란 해당 스레드가 CPU시간을 할당받지 않도록 설정하는 것이다.

스레드 커널 오브젝트 내에는 정지카운트라는 값이 저장되어 있다.

정지카운트가 1이상이면 해당스레드에 CPU시간이 할당되지 않는다.

스레드의 생성을 되짚어 본다.

윈도우즈 OS에서 C/C++ 함수호출로 스레드를 생성하는 방법은 CreateThread 하나 뿐이다.

윈도우즈 OS에서, std::thread, CRT의 _beginthreadex 또한 CreateThread를 호출한다는 사실을 기억해야한다.

CreateThread를 호출하면, 정지카운트는 1로 초기화 된다.

 

스레드초기화가 끝나면 정지카운트가 0이 되고 CPU에 스케쥴링 될 것이다.

만약 함수에 CREATE_SUSPENDED 플래그가 전달되었다면, 스케쥴링 되지 않는다.

 

이렇듯 스레드 정지상태란 단순하게 CPU에 스케쥴링 되지 않도록 설정된 상태이다.

따라서 스레드의 재개란 정지상태에서 다시금 CPU시간을 받도록 설정됨을 의미한다. 

 

스레드의 정지는 SuspendThread함수를 사용하며, 재개는 ResumeThread를 사용한다.

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-resumethread

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-suspendthread

 

SuspendThread를 사용할 때에는 주의사항이 있다.

 

해당 스레드가 자신의 스레드의 컨텍스트 내에서  SuspendThread 함수를 호출하는것은 괜찮다.

허나 다른 스레드에서 SuspendThread를 호출 하는 경우가 문제를 만들게 된다. 

 

힙으로부터의 메모리할당에는 lock 작업이 포함되어있다.

생각해보면 매우 당연한 이야기이다.

여러개의 스레드로부터 동적할당이 일어나도 적절하게 대처하려면 그렇게 해야한다.

 

문제가 바로 여기서 발생한다.

어떤 스레드가 힙으로부터 메모리를 할당받다가 다른스레드의 SuspendThread 함수호출에 의해 정지상태가 되면,

힙은 그대로 잠긴 상태가 된다.

 

이 잠긴 상태에서

다른 스레드가 힙으로부터 메모리를 할당 받으려고 하면 해당 스레드는 수행 정지된다.

 

스레드 슬립 및 타임슬라이스 양보

 

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleep

Sleep

Sleep 함수를 호출한 스레드는 일정시간 동안 자신을 스케쥴 하지 않도록 한다.

또한 자신에게 남은 CPU시간(타임슬라이스라고 부른다.) 를 자발적으로 포기한다.

당연히 상태는 해당 시간동안 정지상태가 된다.

매개변수로 INFINITE를 전달하면 절대로 다른 스레드가 ResumeThread를 호출해줄 때 까지

스케쥴링 되지 않는다.

 

Sleep 시간을 지정할때에는 시스템 클락의 resolution에 유의한다.

아무것도 설정하지 않은 상태로 Sleep(1)을 실행하면, 약 15ms 정도 스케쥴링 되지 않는다.

스케쥴링 되는 틱 레이트인 약 15ms 보다 Sleep에 전달된 인자인 1이 더 작기 때문에 생기는 일이다.

이를 해결하기 위해서는 Sleep이 호출되는 시점보다 이전에,

timeBeginPeriod를 호출하도록 한다.

그리고 App의 종료시 timeEndPeriod를 호출하도록 한다.

 

timeGetDevCaps 함수를 호출하면 시스템이 지원하는 가장 작은

틱 레이트(time resolution)를 얻어올 수 있다. 

 

Sleep(0)의 경우, 호출한 스레드의 우선순위와 같거나 우선순위가 큰 스레드

Ready상태의 스레드에게 타임 슬라이스를 넘겨준다.

단순히 타임슬라이스를 포기하는 것이므로 Waiting 상태가 아닌 Ready 상태가 된다.

즉, 해당 스레드는 그대로 스케쥴이 계속된다.

우선순위가 같거나 큰 스레드가 없으면 호출한 스레드가 다시 스케쥴 될 수 있다.

 

SwitchToThread

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-switchtothread

 

Sleep(0)와 Ready 상태의 스레드를 스케쥴링 하게 하는 역할은 같지만 동작이 다르다.

Sleep(0)는 호출한 스레드보다 우선순위가 크거나 같은 스레드에게만 타임슬라이스를 양보한다.

SwitchToThread는 상관없이 같은 프로세서 내에서 일정시간 동안 타임슬라이스를 받지 못한

스레드에게 양보한다.

즉 우선순위가 낮은건 관계없다.

같은 프로세서 내라는 것이 중요하다.

 

 

YieldProcessor

타임슬라이스를 포기하고

스레드가 현재 실행되고 있는 프로세서에서 수행될 다른 스레드들에게 양보한다.

msdn 에 따르면, 인텔의 하이퍼 스레딩과 같이 하나의 프로세서가

여러개의 스레드들을 실행할 수 있는 기술이 필요하다.

 

스레드 우선순위

윈도우즈에서 스레드들은 0~31 까지의 우선순위 값을 가진다.

기본적으로, 만약 상대적으로 우선순위가 높은 스레드가 스케쥴 가능 상태라면,

상대적으로 우선순위가 낮은 스레드들은 절대로 타임슬라이스를 얻지 못하게 되어있다. 

 

그래도 시스템의 많은 스레드들이 타임 슬라이스를 양보하고, 일정시간 스케쥴 불가능 상태를 유지하곤 하기 때문에,

그 시간동안 낮은 우선순위를 가진 스레드들이 실행될 수 있다.

 

허나, 낮은 우선순위의 스레드가 일단 타임슬라이스를 얻어서 실행되는 중이더라도,

높은 우선순위를 가진 스레드가 스케쥴 가능 상태가 되면,

즉시 남은 타임 슬라이스를 높은 우선순위를 가진 스레드에게 양보해야 한다.

 

낮은 우선순위를 가진 스레드가 제대로 된 타임 슬라이스를 얻는 것이 굉장히 어려워 보인다.

이렇듯 우선순위가 높은 스레드들이 타임슬라이스를 독식하여

낮은 우선순위를 가진 스레드들이 실행되지 못하는 이러한 상태를 starvation(기아) 상태라고 부른다.

 

점수로 우선순위를 매기는 이런 방식이 유연하지 못할 가능성이 있다. 

 

때문에 시스템은 스레드의 우선순위에 상당히 간섭을 한다.

상술한 기아상태 방지를 위해서, 일정시간 동안 스케쥴링 가능상태임에도 타임 슬라이스를

얻지 못한 스레드들의 우선순위를 일시적으로 상승시켜 스케쥴링을 유도한다.

 

또한, I/O 요청에 대한 완료 통지 혹은 윈도우즈 메시지 통지를

수행할 때에도 스레드의 우선순위를 잠시 일시적으로 상승시킨다.

낮은 우선순위의 스레드가 요청한 I/O에 대한 반응을 빠르게 유도하기 위함이다.     

 

 

스레드의 우선순위 레벨은 프로세스 우선 순위 클래스와 연관되어있다.

아래의 표를 보도록 하자.

 

 

표를 보면 프로세스 우선순위 중 가장 우선순위가 높은 Realtime 만이 15이상의 값을 가질 수 있다.

가급적 프로세스 우선순위를 Realtime 으로 설정하는 것은 피해야 한다.

시스템에서 굴리는 프로세스내의 스레드 스케쥴링에 영향을 줄 수 있기때문이다.

 

표에 표시되지 않은

17 18 19 20 21 27 28 29 30 등은 유저모드에서 사용될 수 없다.

커널 모드에서 사용되는 디바이스 드라이버를 작성할때에만 사용할 수 있다.

 

SetPriorityClass

프로세스의 우선순위를 설정할 때에는

SetPriorityClass 함수를 이용한다.

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass

 

SetThreadPriority

스레드의 우선순위를 설정할때에는

SetThreadPriority 함수를 이용한다.

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadpriority

 

 

SetProcessPriorityBoost

상술했듯 기아상태 유발의 위험성과 반응 I/O등 이벤트에 대한 반응성 문제로,

시스템은 동적으로 스레드 우선순위를 조정하기도 한다.

이 작업은 스레드 우선순위 1~15의 스레드 내에서만 적용된다.

그 이상 높이면 운영체제의 동작에 영향을 미칠 가능성이 커지기 때문이다.

이러한 동적 우선순위 조정을 금지하도록 설정할 수 있다.

이 작업은 SetProcessPriorityBoost 함수를 이용한다.

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setprocesspriorityboost

 

프로세서 선호도

윈도우즈 OS는 기본적으로 soft affinity를 사용한다.

다른 조건이 동일하다면, 마지막으로 스레드를 실행했던 프로세서에

스레드를 다시 스케쥴링 하도록 한다.

이전과 동일한 프로세서에서 스레드가 수행될 경우 동일한 캐시메모리를

재사용할 수 있는 가능성이 있기 때문이다.

 

이와 별개로 강제로 해당 프로세서에서만 수행되도록 하는 방법이 있는데,

이것을 hard affinity라고 부른다.

 

이는 SetProcessAffinityMask와 

https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setprocessaffinitymask

 

SetThreadAffinityMask를 이용한다.

https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setthreadaffinitymask

 

이 프로세서 선호도는 사용시 우선순위 레벨과 비교해 유의해야한다.

사용 프로세서가 한정되기 때문에, 설정한 우선순위가 무의미해질 수 있다.

 

높은 우선순위를 가진 스레드라고 하더라도,

낮은 우선순위를 가진 스레드가 사용하고 있는 프로세서가 사용 프로세서로

설정되지 않았다면, 해당 타임 슬라이스를 강제로 얻어올 수 없고, 

먼저 수행되도록 강제하는 우선순위 레벨 설정의 의미가 사라져 버린다.