내가 처음 IOCP 서버를 다루기 시작할 때, 다음과 같은 코드가 이상해 보였다.
void ProcPack()
{
shared_ptr<Packet> pPack = nullptr;
char* pStart = nullptr;
UINT8 Size;
while (m_bIsRunProcThread)
{
if (!m_PackQ.Dequeue(pPack))
{
this_thread::sleep_for(chrono::milliseconds(1)); // made it wait, not sleeping for arbitrary time
continue;
}
// ... do something
}
}
분명 스레드를 신호에 따라 멈추고 다시 동작하게 만드는 함수들이 있는데도,
1 ms만 쉬고 다시 다음 라운드를 돌게 만드는 거였다.
왜?
SNS에 질문해 보니 누군가가 '시스템 콜'에 대해 알아보라고 했다.
위 코드에서 1 ms 쉬고 다시 다음 라운드에서 유의미한 작업을 할지 다시 판단하는 걸 보고 'Busy Wait'라 부른다.
왜 이런 행위를 하냐!
저 코드는 동작하는 스레드의 멈춤/실행을 제어한다.
'기다려' 했을 때 정말 스레드가 잠자기에 들어가는 게 아니라,
1 ms 쉬고 다시 검사하면서 계속 돌아가는 식.
요즘 러닝이 대세던데, 횡단보도 빨간 불 앞에서 러너들이 제자리 뛰기를 하며 기다리는 상황이라 보면 된다.
러너는 달리기 페이스가 깨지지 않기 위해 이렇게 하는 것 같은데, 스레드에서 Busy Wait를 하는 이유도 비슷하다.
만약 스레드가 잠시 동작을 멈추도록 std::mutex
으로 객체를 잠그거나 std::condition_variable
등을 사용하면,
CPU는 라운드를 돌다 말고 커널에 들어가 스레드를 재운다.
깨울 때도 마찬가지.
이를 시스템 콜, 커널 호출이라 부른다.
딱 들어만 봐도 Busy Wait로 무의미한 반복문을 도는 것보다 오버헤드가 커 보인다.
반면 Busy Wait는 무의미한 반복문을 도는데 CPU가 따끈해지는 단점이 있다.
Busy Wait로 구현할 경우 프로세스는 사용자 모드(user mode)에서만 동작.
반면 시스템 콜이 일어나는 std::mutex
, std::condition_variable
은 커널 모드(kernel mode)에서 동작이 일어난다.
또, 위 코드에서 Busy Wait를 할 시 1 ms를 쉬고 다시 돌게 만들었는데,
만약 쉬지 않고 계속 조건문을 검사하도록 하면 CPU엔 조건문 검사하는 작업만 스케쥴링 된다.
저 쉬고 오는 동안 컨텍스트 스위칭을 하여 다른 작업들도 할 수 있도록 한 것이다.
그러면, 실무에서 다음과 같이 판단할 수 있을 것이다.
실행/잠자기 전환이 빈번하다? -> Busy Wait!!!
빈번하지 않고, 하나의 라운드가 무거운 작업이다? -> std::mutex
, std::condition_variable
과 같은 시스템 콜이 있는 도구들!!!