나는 CWnd::SetTimer() 사용시 언제부터인가 다음의 코드를 즐겨 사용하였다.
[CODE]
....
m_iTimerID = SetTimer( DEFINED_ID /*timer ID*/ ,1000 /*time-out value*/, NULL);
....

xClass::OnTimer(UINT nIDEvent)
{
    if(nIDEvent == DEFINED_ID)
    {
       .....
    }

    CWnd::OnTimer(nIDEvent);
}

....
KillTimer(m_iTimerID);
....
[/CODE]

아마 이런 코드를 즐겨 사용하게 된것이 근래 2년 이내의 최근이지 싶다.

MFC를 사용한지 족히 10년 이상이 되어가는것 같은데, 오늘에서야 이 코드가 정말 잘못된 코드임을 알게 되었다.

천만 다행인지 불행인지 OnTimer() 함수에서 nIDEvent 를 m_iTimerID와 비교하지 않아 그나마 프로그램이 오동작 하지 않고 버그를 갖고 있는지 꿈에도 모른채 지내오지 않았나 싶다. 오늘 전혀 엉뚱한 이유로 이 사실을 알게되었기 때문이다.

DirectX의 Multimedia Timer를 써보니 리턴 값으로 생성된 Timer의 ID를 리턴해주는 것이 경우에 따라 정말 편할 수 있다는 것을 경험하였다. Timer가 동시에 수개에서 수십개가 설정되어야 하고 수시로 해제되고 다시 설정되어야 하는 상황에서 미리 할당할 ID를 정해 두고 SetTimer(pre_defined_id, ...)을 호출해야 한다는 것에 불편함을 느꼇다. 그래서 Multimedia Timer처럼 따로 ID를 정해주지 않아도 생성하고 나면 ID를 리턴해주었으면 하는 바램이 생겼다.

(이하 Debug 모드에서 수행하며 얻은 결과이다)
아래와 같이 하면 되지 않을까? 하여 별 생각없이 다음의 코드를 사용해 보았다.
[CODE]

xClass::StartTimer()
{
    if(m_iTimerID == 0)
           m_iTimerID = SetTimer( 0, 500, NULL);
}

xClass::StopTimer()
{
    if(m_iTimerID > 0)
    {
           KillTimer(m_iTimerID);
           m_iTimerID = 0;
     }
}

xClass::OnTimer(UINT nIDEvent)
{
    if(nIDEvent == m_iTimerID)
    {
       .....
    }

    CWnd::OnTimer(nIDEvent);
}
[/CODE]

헉! 그런데 정말 재미 있는 현상이 발생하였다.

StartTimer()를 수행하면 SetTimer를 잘 수행하여 m_iTimerID 가 1을 리턴 받았는데도 불구하고 OnTimer에서 내가 하라고 작성한 코드가 실행되지 않는 것이다.

"어라?! 어라?!" 이번엔 StopTimer()를 호출하니 역시 KillTimer를 수행하고 나서 OnTimer의 내가 수행해 달라고 작성해 놓은 코드가 수행되는 것이 아닌가?

이쯤 되니 기동안 쉬피 받던 SetTimer() 함수를 MSDN에서 찾아보지 않을 수 없었다.

CWnd::SetTimer

  Return Value  

The timer identifier of the new timer if the function is successful. An application passes this value to the KillTimer member function to kill the timer. Nonzero if successful; otherwise 0.

헉! 결론적으로 이건 사실이 아니었다.

차라리 Win32 API SetTimer의 리턴값 정의 부분이 사실에 가까운것 같다.

If the function succeeds and the hWnd parameter is NULL, the return value is an integer identifying the new timer. An application can pass this value to the KillTimer function to destroy the timer.

If the function succeeds and the hWnd parameter is not NULL, then the return value is a nonzero integer. An application can pass the value of the nIDEvent parameter to the KillTimer function to destroy the timer.

CWnd::SetTimer가 Win32 API를 그대로 호출한다고 할때, 2번째 경우에 해당할 것이다. 즉 리턴 값은 0이 아닌 값이며 KillTimer시 리턴 값을 넘겨주면 안되고 SetTimer의 첫번째 파라미터에 사용하였던 값인 nIDEvent 값을 넘겨주어야 한다는 것이다.

첫번째 경우라면 내가 사용한 코드가 맞는 코드이지만 두번째 경우라면 잘못된 코드인 것이다.
그리고 비로소 StartTimer() 호출에는 동작하지 않고 StopTimer() 호출시에는 죽으라는 타이머는 죽지 않고 신기하게 그때 부터 동작하는 것이 설명되었다.

[CODE]m_iTimerID = SetTimer( 0, 500, NULL);[/CODE]

Win32 API의 SetTimer() 설명을 따른다면 SetTimer를 통해 내가 설정한 타이머의 ID는 리턴 값인 1이 아닌 첫번째 파라미터로 넘겨준 0이 었던 것이다.

따라서 OnTimer에서 nIDEvent에는 0이 넘어 오는데 m_iTimerID의 1과 비교하여 원하는 동작을 하도록 하니 WM_TIMER이벤트는 정상 발생하나 내가 수행되리라 생각한 코드에는 도달하지 못했던 것이다.

[CODE]
xClass::OnTimer(UINT nIDEvent)
{
    if(nIDEvent == m_iTimerID)
    {
       .....
    }

    CWnd::OnTimer(nIDEvent);
}
[/CODE]

그리고 StopTimer()를 호출했을 때 운좋게도

[CODE]
xClass::StopTimer()
{
    if(m_iTimerID > 0)
    {
           KillTimer(m_iTimerID);
           m_iTimerID = 0;
     }
}
[/CODE]

KillTimer로 1을 넘겨주어 원하는 타이머는 제거하지 못하여 WM_TIMER가 계속 발생하는 상황에서 m_iTimerID를 0으로 설정해 줌으로서 OnTimer의 조건문을 통과하여 원하지 않는 시점에 내가 원하는 코드가 수행 되었던 것이다.

바로 비교적 여러 개체에서 OnTimer가 구현된 최근 개발 소스를 디버깅 해보니 SetTimer의 리턴값이 첫번째 넘겨준 파라미터와 일치하지 않음을 확인할 수 있었다.

사용자 삽입 이미지


따라서 Multimedia Timer처럼 생성된 Timer의 ID를 리턴 값으로 받고자 한다면, Win32 API SetTimer를 사용하고 이때 HWND 핸들은 NULL로, 그리고 CallBack 함수를 사용하면 될것이다. 부득이 CallBack에 CWnd 상속 개체를 넘겨주어야 한다면 static 등으로 포인터 변수를 선언하고 이를 통해 넘겨주든지 하여 목적을 이룰 수 있을 것이다.

오늘 또한 무심코 KillTimer를 Dialog의 객체 파괴자에 넣은 덕분에 새로운 사실을 알게되었다. --; 이것 또한 최근의 Timer를 사용한 코드를 보니 더러 객체 파괴자에서 사용 되었는데 그동안 문제를 몰랐다는게 신기할 따름이다.
(ASSERT()매크로에 의해 발생하는 듯 하다. 참고로 Release 모드에서는 발생하지 않는다.)

이 문제와 딱 일치하는 재미 있는 물음과 답변의 글이 있다.
Where to put KillTimer() in Dialog

정답의 요지는 Timer에 연결된 창(HWND)이 있을 경우 해당 창이 파괴되기 전에 KillTimer를 호출해야 하기 때문에 CDialog 뿐만 아니라 CWnd를 상속받은 모든 개체에서 KillTimer 호출시에는 WM_DESTROY 핸들러(OnDestroy) 에서 호출해주는 것이 좋다는 의견이었다. 이미 창(HWND)이 파괴된 후인 CWnd를 상속받은 개체의 파괴자에서 KillTimer를 호출하면 Assertion을 만나게 되는 이유도 이 때문이라는 것이다.


CWnd::KillTimer()가 Window창이 파괴되기 전에 호출되어야 하는 이유를 정확히 알기 위해서는 먼저 다음의 문서를 볼 필요가 있을것 같습니다.

C++ 창 개체와 HWND의 관계

이 문서는 윈도우 시스템의 리소스인 창(HWND)와 이를 캡슐화 하고 있는 C++ 창 개체(CWnd)의 관계와 명칭상 어떻게 구별되는지를 극명하게 보여주고 있다.

다음은 MFC의 (C++)창 개체인 CWnd와 윈도우 시스템 리소스 창인 HWND가 생성과 파괴시 어떤 관련이 있는지 좀더 이해할 수 있도록 도와준다.

창 개체(CWnd)

MFC의 소켓 개체이든 이번의 CWnd 개체이든 무언가를 캡슐화 하고 있는 개체는 재미 있는 특징을 가지고 있다.
미묘하게 자신과 자신의 상태값이 존재하는 시점과 자신이 캡술화 하고 있는 것이 존재하는 시점에 시차가 존재하며, 또한 파괴되는 과정에서도 캡출화 하고 있는 것이 파괴되는 시점과 자기 자신이 파괴되는 시점에 시차가 존재한다는 점이다. 그리고 이 시차가 크면 클수록 이를 유념해서 사용하지 않으면 문제에 봉착할 수 있다는 것이다.
이것이 이번 주제를 이해하기 위해 이글에서 짚어 내야할 요지라 생각한다.

소스가 공개되지 않은 --;(mfc71d.dll) CWnd::KillTimer를 유추해 보기 전에 다시한번 CWnd와 HWND를 구별해 보자.

  • HWND : 윈도우 OS System에서 View영역과 Device Context, 사용자 입력등을 처리할 수 있는 기능을 가진 창을 나타내는 불투명한 구조체 리소스
  • CWnd : HWND를 캡슐화 하고 있는 C++ 개체

그럼 이상으로 CWnd::KillTimer의 내부를 유추해 보자.
Debug Assert 에러를 내보내는 것을 보면 분명 다음과 같이 되어 있으리라 생각한다.

[CODE]
BOOL CWnd::KillTimer(UINT_PTR nIDEvent)
{
        ASSERT(m_hWnd != NULL && ::IsWindow(m_hWnd))

        return ::KillTimer(m_hWnd, nIDEvent);
}
[/CODE]

정말 위처럼 코딩이 되어 있다면 Window 창인 HWND가 해제된 다음에 KillTimer를 호출하면 문제가 되는 것이 명확해 진다.

다음은 Window OS의 Timer들에 대해 정리해 놓은 글이다 좋은 참고가 될 것으로 생각된다.

Timers tutorial


Standard Win32 Timer가 Kernel 리소스가 아니라는 것은 주목해 볼만 하다. 우리가 일반적으로 쓰는 Win32 Timer와 DirectX의 Multimedia Timer, 그리고 생소한 커널 리소스인 Waitable Timer와 Queue Timer를 잘 설명하고 있다. 다만 글중에서 Win32의 SetTimer 리턴 값에 대해 "The timer identifier, If hWnd is non-NULL, than it is equal to nIDEvent" 는 오늘의 이 글이 있게한 원흉으로서 절대 잘못된 것임을 명심하자.

크리에이티브 커먼즈 라이센스
Creative Commons License
2007/05/19 07:46 2007/05/19 07:46

Trackback Address :: http://www.codeforum.net/blog/pitoosung/trackback/81