. OnIdle

메뉴 항목은 OnInitMenu에서 메뉴가 열릴 때 여러 가지 조건을 보고 상태를 변경한다. 메뉴항목의 상태에 영향을 미치는 클립보드 상황이나 활성창의 유무, 취소 가능성 등의 조건들은 수시로 변하지만 메뉴가 열리기 전에는 이 조건들을 따로 점검해 볼 필요가 없다. 메뉴가 열리기 직전에 항상 WM_INITMENU 메시지가 전달되므로 이 메시지를 받았을 때만 메뉴항목을 제대로 관리하면 메뉴는 항상 현재 상황을 잘 반영한다. 그 전에는 메뉴가 보이지 않으므로 설사 현재 상황과 맞지 않더라도 문제될 것이 없다.

그러나 툴바는 화면에 항상 보이기 때문에 상태를 바꿀 적당한 기회가 따로 없다. 그래서 프로그램의 상황이 바뀌는 즉시 툴 버튼에 바로 반영되어야 한다. 복사, 잘라내기 버튼은 선택영역이 없다가 생기면 즉시 활성화되어야 하고 선택영역이 사라지면 즉시 사용 금지 되어야 한다. 이렇게 툴바가 정확한 상태를 유지하려면 프로그램은 항상 툴바의 상태를 관리해야 한다. 그렇다면 툴바의 상태를 관리하는 OnIdle 함수는 과연 언제 호출되어야 할까? 이 문제는 윈도우즈의 메시지 시스템을 이해하는데 아주 좋은 예가 되므로 여러 가지 방법을 시도해보고 상세하게 실습을 해보도록 하자.

첫 번째로 타이머가 가장 먼저 생각난다. 1초나 0.5초에 한 번꼴로 OnIdle을 호출함으로써 주기적으로 툴바의 상태를 관리하면 툴바는 거의 정확하게 현재 상태를 반영할 수 있다. 타이머의 주기가 짧을수록 정확성은 더욱 높아진다. 충분히 가능한 방법이기는 하지만 실행시간의 낭비가 심하고 타이머 주기를 충분히 짧게 해도 반응이 즉각 나타나지 않을 수 있다. 왜냐하면 타이머는 우선 순위가 끝에서 두 번째로 늦은 메시지이기 때문이다.

두 번째로 멀티 스레드를 쓸 수도 있다. 툴바의 상태를 관리하는 별도의 스레드를 만들고 이 스레드를 낮은 우선 순위로 계속 실행시키면 된다. 메인 스레드의 작업에 큰 영향을 주지 않고 매끄럽게 툴바의 상태를 관리할 수는 있지만 왠지 좀 어울리지 않는다는 생각이 든다. 닭잡는데 소잡는 칼을 쓰는 격이다.

툴바의 상태를 관리하는 작업은 정확하면 좋기는 하지만 프로그램의 성능을 저하시켜 가면서까지 정확해야 할 필요는 없다. 짧은 순간이라면 약간의 불일치가 발생하더라도 큰 지장이 없는 작업이다. 그래서 툴바의 상태관리는 실행 시간의 낭비를 최소화하면서 반응이 가장 빠른 방법을 사용해야 하는데 그 방법이 바로 OnIdle이다. 앞에서 이미 툴바 상태관리 함수의 이름을 OnIdle로 작성해서 눈치를 챘겠지만 말이다.

아이들 타임이란 아무 것도 하지 않고 빈둥빈둥 놀고 있는 시간을 의미한다. 사실 응용 프로그램은 거의 90% 이상의 시간을 빈둥거리며 놀고 있는데 작업 관리자로 확인해보면 과연 그렇다는 것을 알 수 있다. 이 노는 시간을 잘 활용하면 툴바의 상태관리나 불필요한 자원의 회수(Garbage Collection) 등의 유용한 작업들을 할 수 있다.

이 남는 시간을 활용하자면 먼저 언제가 노는 시간인지, 프로그램이 한가해지는 시점이 언제인가를 찾아야 한다. 노는 시간이 되면 저 한가합니다. 아이 심심해라는 메시지를 따로 보내 주는 것이 아니므로 이 시간을 직접 찾아야 한다. 노는 시간은 메시지 루프에서 찾을 수 있는데 GetMessage 함수가 큐에서 아무런 메시지도 꺼내지 못하고 있는 상태, 즉 메시지 큐가 비어 있는 상태가 바로 노는 시간이다.

이 시간을 아이들 타임이라고 하는데 GetMessage 함수는 아이들 타임이 되면 다른 스레드로 실행 시간을 양보하도록 되어 있다. 그래서 한 프로세스가 한가해지면 다른 프로세스가 CPU 시간을 더 많이 사용할 수 있게 되고 GetMessage 함수의 이런 특성에 의해 멀티태스킹이 부드러워진다. 아이들 타임을 다른 스레드에게 양보하기 전에 내가 먼저 할 일이 있다면 이 시간을 활용할 수 있는데 이런 일을 하는 핸들러를 통상 OnIdle이라는 이름으로 작성한다. 현재의 메시지 루프를 보자.

 

     while(GetMessage(&Message,0,0,0)) {

          if (!IsWindow(g_FindDlg) || !IsDialogMessage(g_FindDlg,&Message)) {

              if (!TranslateMDISysAccel(g_hMDIClient, &Message)) {

                   if (!TranslateAccelerator(hWnd,hAccel,&Message)) {

                        TranslateMessage(&Message);

                        DispatchMessage(&Message);

                   }

              }

          }

     }

 

여기까지만 해도 과히 간단하지 않다. GetMessage는 메시지 루프가 비어 있으면 다른 스레드로 실행시간을 양보하며 메시지가 들어올 때까지 무한 대기한다. 그래서 이 함수로 메시지 큐를 읽어서는 아이들 타임을 얻을 수 없다. 다음과 같이 변형해보자.

 

     for (;;) {

          if (PeekMessage(&Message,NULL,0,0,PM_REMOVE)) {

              if (Message.message==WM_QUIT)

                   break;

              if (!IsWindow(g_FindDlg) || !IsDialogMessage(g_FindDlg,&Message)) {

                   if (!TranslateMDISysAccel(g_hMDIClient, &Message)) {

                        if (!TranslateAccelerator(hWnd,hAccel,&Message)) {

                            TranslateMessage(&Message);

                            DispatchMessage(&Message);

                        }

                   }

              }

          } else {

              OnIdle();

          }

     }

 

GetMessage로 메시지를 바로 읽지 않았으며 PeekMessage 함수로 메시지 큐를 먼저 검색해 본다. 큐에 메시지가 있으면 이 메시지를 읽어서 통상적인 방법대로 처리하면 된다. 만약 메시지 큐가 비어 있으면 PeekMessage 함수는 FALSE를 리턴하는데 이때가 바로 아이들 타임이다. 여기서 OnIdle을 불러주면 노는 시간을 활용해서 툴바의 상태를 관리할 수 있다.

PeekMessage 함수는 프로그램을 끝내라는 신호인 WM_QUIT를 따로 관리하지 않으므로 WM_QUIT는 직접 처리해야 한다. 읽은 메시지가 WM_QUIT이면 프로그램을 종료하기 위해 메시지 루프를 탈출한다. 실행해보면 툴바의 상태가 제대로 관리될 것이다. 선택영역 유무에 따라 복사, 잘라내기 버튼의 상태가 바뀌며 자동개행 여부가 툴 버튼에 표시된다.

반응도 바로바로 나타나고 멋지다. 그런데 과연 정말 멋지기만 할까? OnIdle에 다음 임시 테스트 코드를 작성해보자.

 

void OnIdle()

{

     HWND hActive;

     SInfo *pSi;

     int s,e;

 

     static int count;

     TCHAR szTemp[256];

     count++;

     wsprintf(szTemp,"OnIdle-%d",count);

     SetWindowText(g_hFrameWnd,szTemp);

     ....

 

스태틱 변수 count를 선언하고 매번 1씩 증가시켜 타이틀바에 그 값을 출력하도록 했다. 이 함수의 호출 횟수를 확인해보는 테스트 코드이다. 이 코드를 작성한 후 실행해보면 타이틀바에 카운트가 무서운 속도로 증가하는 것을 확인할 수 있으며 눈깜짝할 새에 10만 번에 도달할 것이다. 이 상태에서 작업 관리자로 확인해보면 당근의 CPU 점유율은 항상 100%이다.

그러나 OnIdle 함수가 이렇게 자주 호출되더라도 자기 자신의 실행속도 감소는 거의 없다. 왜냐하면 아무 메시지도 처리하지 않을 때인 아이들 타임에만 OnIdle을 호출하며 자기가 해야 할 일이 방해를 받지는 않기 때문이다. 문제는 당근과 함께 실행되고 있는 다른 프로그램들이 제대로 CPU 시간을 할당받지 못한다는 점이다.

아이들 타임은 당근의 입장에서는 버려진 시간이지만 다른 프로세스에게는 이 시간이 귀중할 수도 있다. 더 심각한 문제는 당근이 포커스를 가지고 있지 않을 때도 OnIdle을 반복적으로 호출함으로써 전체적인 시스템 성능을 저하시키고 있다는 점이다. 그래서 당근은 자기 자신의 작업을 위해 CPU 시간 전체를 소모해버리는 이기적인 욕심꾸러기가 되어 버렸다. 멀티태스킹 시스템이란 실행되는 모든 프로세스의 협조에 의해 원활히 돌아가는 것이므로 이런 식으로 프로그램을 작성해서는 안된다.

게다가 툴 버튼의 상태는 자주 바뀌지 않는데도 불구하고 일초에 거의 수천 번씩 너 선택영역 있어, 취소는 가능해?라고 불필요한 질문을 하고 있는 중이다. 작업 결과는 정확해졌지만 당근의 지능 지수는 더욱 멍청해져 버렸는데 좀 더 지능적으로 OnIdle 함수 호출을 관리하도록 조치를 취하도록 하자.

첫 번째 조치는 아무리 할 일이 없는 아이들 타임이라도 두 번 연속 OnIdle을 부르지 않도록 하는 것이다. 다른 메시지를 처리하지 않았다는 것은 프로그램의 상태에 변화가 생기지 않았다는 뜻이므로 OnIdle을 연속적으로 두 번 부르는 것은 거의 의미가 없다. AllowIdle 변수를 추가하고 이 변수가 TRUE일 때만 OnIdle 호출하도록 수정하자.

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

            ,LPSTR lpszCmdParam,int nCmdShow)

{

     BOOL AllowIdle=TRUE;

     ....

     for (;;) {

          if (PeekMessage(&Message,NULL,0,0,PM_REMOVE)) {

              if (Message.message==WM_QUIT)

                   break;

              AllowIdle=TRUE;

              if (!IsWindow(g_FindDlg) || !IsDialogMessage(g_FindDlg,&Message)) {

                   if (!TranslateMDISysAccel(g_hMDIClient, &Message)) {

                        if (!TranslateAccelerator(hWnd,hAccel,&Message)) {

                            TranslateMessage(&Message);

                            DispatchMessage(&Message);

                        }

                   }

              }

          } else {

              if (AllowIdle) {

                   OnIdle();

                   AllowIdle=FALSE;

              }

          }

     }

 

이렇게 수정한 후 다시 실행해보자. 카운트 증가 속도가 눈에 띄게 감소할 것이다. OnIdle이 한 번 호출되면 AllowIdle FALSE가 되며 이 값은 PeekMessage가 메시지 큐에서 메시지를 한 번 꺼내야 TRUE가 된다. AllowIdle TURE일 때만 OnIdle이 호출되므로 다른 메시지를 처리할 때마다 OnIdle이 한 번씩 호출된다. OnIdle이 메시지 처리 사이 사이에 끼어 드는 셈이다.

그러나 OnIdle 호출 횟수가 줄어들어도 CPU 점유율은 여전히 100%를 자랑한다. 그 이유는 PeekMessage 함수가 반복적으로 호출되고 있기 때문이다. 무한루프를 실제로 무한히 돌리고 있으니 쉬어갈 틈이 없고 따라서 CPU가 한가할 틈이 없는 것이다. PeekMessage 함수는 다른 프로세스에게 시간을 전혀 양보하지 않고 메시지 큐만 열심히 점검하는 특성이 있다. CPU 점유율을 낮추기 위해 다른 프로세스에게 시간을 양보할 줄 아는 착한 GetMessage 함수를 다시 채용한다. 최종 완성 코드는 다음과 같다.

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

            ,LPSTR lpszCmdParam,int nCmdShow)

{

     BOOL AllowIdle=TRUE;

     ....

     for (;;) {

          do {

              if (!GetMessage(&Message,NULL,0,0))

                   goto endloop;

              if (Message.message != 0x118/*WM_SYSTIMER*/) {

                   AllowIdle=TRUE;

              }

              if (!IsWindow(g_FindDlg) || !IsDialogMessage(g_FindDlg,&Message)) {

                   if (!TranslateMDISysAccel(g_hMDIClient, &Message)) {

                        if (!TranslateAccelerator(hWnd,hAccel,&Message)) {

                            TranslateMessage(&Message);

                            DispatchMessage(&Message);

                        }

                   }

              }

          } while (PeekMessage(&Message,NULL,0,0,PM_NOREMOVE));

 

          if (AllowIdle) {

              OnIdle();

              AllowIdle=FALSE;

          }

     }

 

endloop:

     if (hMutex) {

          CloseHandle(hMutex);

     }

     CoUninitialize();

     if (bUninstall) {

          Uninstall();

     }

     return (int)Message.wParam;

}

 

전체 메시지 루프는 for 무한루프로 구성되어 있고 이 안에 do~while 루프와 OnIdle 호출문이 작성되어 있다. do~while 루프는 메시지 큐에 있는 모든 메시지를 처리할 때까지 반복되며 모든 메시지를 다 처리하면 그제서야 OnIdle을 한 번 호출한다. OnIdle 처리 후 다시 do 루프로 돌아오면 다음 메시지가 들어올 때까지 GetMessage 함수가 메시지를 대기하게 되며 이때 다른 프로세스로 실행 시간을 양보한다.

do 루프는 while의 조건과는 상관없이 최소한 한 번은 실행되므로 설사 메시지 큐가 비어 있더라도 PeekMessage 함수보다 GetMessage 함수가 먼저 메시지 큐를 보게 되며 이때 다른 프로세스로의 양보가 발생하는 것이다. 하나의 메시지를 처리하면 AllowIdle TRUE가 되는데 단 캐럿을 깜박거리는 0x118 메시지는 OnIdle 호출 조건에서 제외하였다. 캐럿이 깜박거린다고 해서 툴 버튼의 상황이 달라지지는 않기 때문이다.

이제 다시 실행해보면 CPU 점유율이 뚝 떨어질 것이다. 당근은 대부분의 시간을 GetMessage에서 빈둥거리고 놀고 있으며 하나의 메시지를 처리하면 OnIdle을 딱 한 번 호출하여 툴 바의 상태를 관리하므로 속도나 정확성 모두를 만족시키고 있다. OnIdle에 작성한 카운트 코드는 이제 삭제하도록 하자. 차후에도 아이들 타임에 할 일이 있다면 OnIdle 함수에 코드를 작성하면 된다.

OnIdle 처리를 위해 WinMain의 메시지 루프가 상당히 복잡해 졌는데 여기서 작성한 루프가 반드시 모든 경우에 효율적인 것은 아니다. 프로그램의 상황에 따라 메시지 루프도 다양하게 변경 가능한 프로그래밍 대상이므로 실제 문제 해결에 적합한 메시지 루프를 잘 찾아 보아야 한다. 참고로 당근이 채용하고 있는 메시지 루프는 MFC의 메시지 루프를 참조하여 만든 것이되 그보다는 조금 더 단순한 형태로 되어 있다.