Idle Processing

段劲
2023-12-01

Idle Processing

Because MFC's application class, CWinApp, provides the message loop that retrieves and dispatches messages, it's a simple matter for CWinApp to call a function in your application when no messages are waiting to be processed. If you look at the source code for the CWinThread::Run function that gets called by WinMain to start the message loop, you'll see something that looks like this:

BOOL bIdle = TRUE;
LONG lIdleCount = 0;

for (;;)
{
     while (bIdle &&
         !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
    {
         if (!OnIdle(lIdleCount++))
            bIdle = FALSE;
    }

    do
    {
        if (!PumpMessage())
            return ExitInstance();

        if (IsIdleMessage(&m_msgCur))
        {
            bIdle = TRUE;
            lIdleCount = 0;
        }

    } while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
}

Before it calls PumpMessage to retrieve and dispatch a message, Run calls ::PeekMessage with a PM_NOREMOVE flag to check the message queue. If a message is waiting, ::PeekMessage copies it to an MSG structure and returns a nonzero value but doesn't remove the message from the queue. If no messages are waiting, ::PeekMessage returns 0. Unlike ::GetMessage, ::PeekMessage doesn't wait for a message to appear in the message queue before returning; it returns immediately. If ::PeekMessage returns nonzero, indicating that messages are waiting to be processed, CWinThread::Run enters a do-while loop that calls CWinThread::PumpMessage repeatedly to retrieve and dispatch the messages. But if ::PeekMessage returns 0 and the bIdle flag is set, CWinThread::Run calls a member function named OnIdle to give the application an opportunity to perform idle processing. Because OnIdle is a virtual function and because CWinApp is derived from CWinThread, a derived application class can hook into the idle loop by replacing CWinApp::OnIdle with an OnIdle function of its own.

Back in the days of Windows 3.x, when applications were inherently single-threaded, OnIdle was the perfect place to perform background processing tasks such as print spooling and garbage collecting. In 32-bit Windows, CWinApp::OnIdle's usefulness is greatly diminished because low-priority tasks can be performed more efficiently in background threads of execution. OnIdle still has legitimate uses, however. MFC uses it to update toolbar buttons and other always-visible user interface objects by calling update handlers registered in the message map. It also takes advantage of times when the application isn't busy processing messages by deleting temporary objects created by functions such as CWnd::FromHandle and CWnd::GetMenu.

When you call FromHandle to convert a window handle into a CWnd pointer, MFC consults an internal table called a handle map that correlates CWnd objects and window handles. If it finds the handle it's looking for, MFC returns a pointer to the corresponding CWnd object. If the window handle doesn't appear in the handle map because a corresponding CWnd doesn't exist, however, FromHandle creates a temporary CWnd object and returns its address to the caller. The next time OnIdle is called (which doesn't occur until after the message handler that called FromHandle returns), MFC cleans up by deleting the temporary CWnd object. That's why the documentation for some MFC functions warns that returned pointers might be temporary and "should not be stored for later use." What that really means is that an object referenced by one of these temporary pointers isn't guaranteed to exist outside the scope of the current message handler because, once that handler returns, OnIdle is liable to be called—and the object deleted—at any moment.

Using OnIdle

An MFC application can enact its own idle-processing regimen by overriding the virtual OnIdle function that it inherits from CWinApp. OnIdle is prototyped as follows:

virtual BOOL OnIdle (LONG lCount)

lCount is a 32-bit value that specifies the number of times OnIdle has been called since the last message was processed. The count continually increases until the message loop in CWinThread::Run calls PumpMessage to retrieve and dispatch another message. The count is then reset to 0 and starts again. WM_PAINT messages, WM_SYSTIMER messages, and certain mouse messages don't cause lCount to be reset. (WM_SYSTIMER is an undocumented message Windows uses internally.) lCount can be used as a rough measure of the time elapsed since the last message or of the length of time the application has been idle. If you have two background tasks you'd like to perform during idle time, one that's high priority and another that's low, you can use lCount to determine when to execute each task. For example, you might perform the high-priority task each time lCount reaches 10 and the low-priority task when lCount hits 100 or even 1,000.

If you could log the calls to an application's OnIdle function without slowing it down, you'd find that 1,000 is not all that high a number. Typically, OnIdle is called 100 or more times per second when the message queue is empty, so a low-priority background task that kicks off when lCount reaches 1,000 is typically executed when the mouse and keyboard are idle for a few seconds. A high-priority task that begins when lCount reaches 10 is executed much more often because the count frequently reaches or exceeds 10, even when the message loop is relatively busy. Idle processing should be carried out as quickly as possible because message traffic is blocked until OnIdle returns.

The value that OnIdle returns determines whether OnIdle will be called again. If OnIdle returns a nonzero value, it's called again if the message queue is still empty. If OnIdle returns 0, however, further calls are suspended until another message finds its way into the message queue and the idle state is reentered after the message is dispatched. The mechanism that makes this work is the bIdle flag in CWinThread::Run, which is initially set to TRUE but is set to FALSE if OnIdle returns FALSE. The while loop that calls OnIdle tests the value of bIdle at the beginning of each iteration and falls through if bIdle is FALSE. bIdle is set to TRUE again when a message shows up in the message queue and PumpMessage is called. As a practical matter, you can save a few CPU cycles by returning FALSE from OnIdle if your background processing is complete for the moment and you don't want OnIdle to be called again until the flow of messages resumes. Be careful, however, not to return FALSE before the framework has finished its most recent spate of idle-processing chores and thus deprive it of the idle time it needs.

The cardinal rule to follow when using OnIdle is to call the base class version of OnIdle from the overridden version. The following OnIdle override demonstrates the proper technique. The base class's OnIdle function is called first, and after the call returns, the application performs its own idle processing:

BOOL CMyApp::OnIdle (LONG lCount)
{
    CWinApp::OnIdle (lCount);
    DoIdleWork (); // Do custom idle processing.
    return TRUE;
}

It turns out that the framework does its processing when lCount is 0 and 1. Therefore, an even better approach is to accord higher priority to the framework's OnIdle handler by delaying the start of your own idle processing until lCount reaches a value of 2 or higher:

BOOL CMyApp::OnIdle (LONG lCount)
{
    CWinApp::OnIdle (lCount);
    if (lCount == 2)
        DoIdleWork (); // Do custom idle processing.
    return TRUE;
}

You can see for yourself what MFC does during idle time by examining the source code for CWinThread::OnIdle in Thrdcore.cpp and CWinApp::OnIdle in Appcore.cpp.

Because the OnIdle implementations in the previous paragraph always returns TRUE, calls to OnIdle will continue unabated even if both you and the framework are finished with OnIdle for the time being. The following OnIdle override reduces overhead by returning FALSE when both MFC's idle processing and the application's idle processing are complete:

BOOL CMyApp::OnIdle (LONG lCount)
{
    BOOL bContinue = CWinApp::OnIdle (lCount);
    if (lCount == 2)
        DoIdleWork (); // Do custom idle processing.
    return (bContinue œœ lCount < 2);
}

The fact that application-specific idle processing isn't started until lCount equals 2 means that the framework won't be deprived of the idle time it needs if the application's OnIdle function returns FALSE.

It's important to perform idle processing as quickly as possible to avoid adversely impacting the application's responsiveness. If necessary, break up large OnIdle tasks into smaller, more manageable pieces and process one piece at a time in successive calls to OnIdle. The following OnIdle function begins its work when lCount reaches 2 and continues responding to OnIdle calls until DoIdleWork returns 0:

BOOL CMyApp::OnIdle (LONG lCount)
{
    BOOL bMFCContinue = CWinApp::OnIdle (lCount);
    BOOL bAppContinue = TRUE;
    if (lCount >= 2)
        bAppContinue = DoIdleWork (); // Do custom idle processing.
    return (bMFCContinue œœ bAppContinue);
}

Because DoIdleWork's return value is also used as OnIdle's return value, OnIdle will cease to be called once DoIdleWork has completed its appointed task.

Idle Processing vs. Multithreading

In Chapter 17, you'll learn about another way to perform background tasks that involves multiple threads of execution. Multithreading is a powerful programming paradigm that's ideal for performing two or more tasks in parallel. It's also scalable: on a multiprocessor system containing n CPUs, Windows NT and Windows 2000 will execute up to n threads concurrently by scheduling each to run on a different processor. (Windows 95 and Windows 98, by contrast, force all threads to share a single CPU, even on multiprocessor systems.)

Given the robust multithreading support in 32-bit Windows, it's reasonable to ask when, if at all, you should use idle processing in lieu of multithreading. Here are two answers:

  • When you have background tasks to perform that must execute in the application's primary thread. User interface_related tasks tend to be very thread-sensitive. That's one reason why MFC performs user interface updates in the primary thread.

  • When you have background tasks to perform and the application that you're writing must work in 16-bit Windows as well as in 32-bit Windows. Multithreading is not supported in 16-bit Windows.

In these situations, performing background tasks in OnIdle makes a lot of sense. Under any other circumstances, multithreading is in all likelihood the proper solution.

 类似资料:

相关阅读

相关文章

相关问答