콘솔 프로그램에서의 SetTimer

@codemaru · July 08, 2007 · 5 min read

몇 일 전 콘솔 프로그램에서 타이머를 사용하는 프로그램을 작성할 일이 있었습니다. SetTimer 함수에 첫 번째 인자를 NULL로 넣으면 메시지로 날라오지 않고 콜백으로 동작하죠. 그래서 당연히 그렇게하면 콘솔 프로그램에서도 사용할 수 있다고 생각했습니다. 왠걸. 타이머 콜백이 호출이 되질 않는 것입니다. 코드를 고쳐가며 쌩 쑈를 해보았지만 안되었습니다. 그러다 포기하고 구글님에게 물으러 갔죠.

INFO: SetTimer() Should Not Be Used in Console Applications

구글님은 역시나 저를 실망시키지 않았습니다. 내용의 요는 SetTimer가 동작하기 위해서는 메시지 루프를 포함하고 있어야 한다는 것 입니다. 그동안 저는 SetTimer가 그냥 커널에서 계산해서 호출해 주는 정도로 알고 있었는데, 그게 아니었나 봅니다. 그래서 ReactOS 홈페이지에서 관련 소스를 한번 찾아봤습니다.

ReactOS
ReactOS는 윈도우 클론을 만들어 보자는 취지의 오픈 소스 프로젝트입니다. 그래서 윈도우의 중요한 코어 부분을 직접 구현하는 작업을 하고 있죠. 굉장한 작업이죠. 공개가 하나도 되지 않은 윈도우 운영체제를 리버싱해서 똑같은 것을 만드는 일이니까요. 그런데 참 신기한 사실은 몇 해전 유출되었던 윈도우 2000의 소스 코드와 ReactOS의 소스 코드가 굉장히 유사하다는 점 입니다. 비 공개된 커널 구조체의 필드명 같은 것들 말이죠. 함수명도 마찬가지 입니다. 그 원인이야 어찌됐는 저희한테는 쌩큐죠. 윈도우 소스 코드를 홈페이지를 통해서 볼 수 있으니 말이죠. 저도 예전 유출되었던 윈도우 소스 코드를 ctag으로 만들어서 보곤 했는데 doxygen으로 작업된 것이 백만배 정도는 더 편리 하더군요.
전체적인 함수의 흐름은 다음과 같습니다. 마지막 함수를 보면 삘이 확 오죠. ㅋ
SetTimer -> NtUserSetTimer -> IntSetTimer -> MsgSetTimer

MsgSetTimer 함수의 핵심 부분은 아래와 같이 구현되어 있습니다. 타이머가 발생해야 하는 시간을 현재 시간 기준으로 계산하고 그 타이머 객체를 메시지 큐에 추가하는 것이 전부죠. 이제야 왜 SetTimer에 메시지 큐가 필요한지 이해가 되죠. 돌이켜 생각해 보니 바쁜 커널이 모든 타이머를 일일히 호출해 주는 건 정말 쓸데없는 짓인것 같네요.

KeQuerySystemTime(&CurrentTime);  
Timer->ExpiryTime.QuadPart = CurrentTime.QuadPart   
                                             + (ULONGLONG) Period \* (ULONGLONG) 10000;  
Timer->Period = Period;  
Timer->TimerFunc = TimerFunc;  
InsertTimer(MessageQueue, Timer);

콘솔 프로그램에서 메시지 큐 없이 SetTimer를 써야 한다면 위 글에서 나와있는대로 멀티미디어 타이머를 사용하는 방법이 있습니다. 그것도 여의치 않다면 별도의 스레드에서 직접 호출해 주는 방법 밖에는 없습니다. 그런데 이 때 한 가지 주의 해야 할 점은 멀티미디어 타이머든 자신이 직접 만든 스레드에서 호출하든 이 것들은 원래 스레드 컨텍스트에서 호출되지 않는다는 점 입니다. 따라서 타이머 프로시저가 공유 자원에 접근하는 경우라면 동기화에 신경을 써야 합니다.

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중