Programming Windows with MFC, Second Edition

[Previous] [Next]

Here are a few odds and ends related to multitasking and multithreading that might be useful to you.

Message Pumps

A common misconception programmers have about multithreading is that it makes applications run faster. On a single-processor machine, it doesn't; however, it does make applications more responsive. One way to demonstrate the difference in responsiveness multithreading can make is to write an application that draws a few thousand ellipses in response to a menu command. If the drawing is done by the primary thread and the thread doesn't occasionally take time out to check its message queue and dispatch any waiting messages, input will be frozen until the drawing loop has run its course. If the same application is written so that drawing is done in a separate thread, it will continue to respond to user input while the drawing loop executes.

In a scenario as simple as this, however, multithreading might be overkill. An alternative solution is to use a message pump to keep the messages flowing while the primary thread draws ellipses. Suppose the message handler that does the drawing looks like this:

void CMainWindow::OnStartDrawing () { for (int i=0; i<NUMELLIPSES; i++) DrawRandomEllipse (); }

If NUMELLIPSES is a large number, the program could be stuck for a long time once the for loop is started. You could try adding another menu command that sets a flag and interrupts the for loop, as shown here:

void CMainWindow::OnStartDrawing () { m_bQuit = FALSE; for (int i=0; i<NUMELLIPSES && !m_bQuit; i++) DrawRandomEllipse (); } void CMainWindow::OnStopDrawing () { m_bQuit = TRUE; }

But that wouldn't work. Why not? Because the WM_COMMAND message that activates OnStopDrawing can't get through as long as the for loop in OnStartDrawing executes without pumping messages. In fact, a menu can't even be pulled down while the for loop is running.

This problem is easily solved with a message pump. Here's the proper way to execute a lengthy procedure in a single-threaded MFC program:

void CMainWindow::OnStartDrawing () { m_bQuit = FALSE; for (int i=0; i<NUMELLIPSES && !m_bQuit; i++) { DrawRandomEllipse (); if (!PeekAndPump ()) break; } } void CMainWindow::OnStopDrawing () { m_bQuit = TRUE; } BOOL CMainWindow::PeekAndPump () { MSG msg; while (::PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE)) { if (!AfxGetApp ()->PumpMessage ()) { ::PostQuitMessage (0); return FALSE; } } LONG lIdle = 0; while (AfxGetApp ()->OnIdle (lIdle++)); return TRUE; }

PeekAndPump enacts a message loop within a message loop. Called at the conclusion of each iteration through OnStartDrawing's for loop, PeekAndPump first calls CWinThread::PumpMessage to retrieve and dispatch messages if ::PeekMessage indicates that messages are waiting in the queue. A 0 return from PumpMessage indicates that the last message retrieved and dispatched was a WM_QUIT message, which calls for special handling because the application won't terminate unless the WM_QUIT message is retrieved by the main message loop. That's why PeekAndPump posts another WM_QUIT message to the queue if PumpMessage returns 0, and why the for loop in OnStartDrawing falls through if PeekAndPump returns 0. If a WM_QUIT message doesn't prompt an early exit, PeekAndPump simulates the framework's idle mechanism by calling the application object's OnIdle function before returning.

With PeekAndPump inserted into the drawing loop, the WM_COMMAND message that activates OnStopDrawing is retrieved and dispatched normally. Because OnStopDrawing sets m_bQuit to TRUE, the drawing loop will fall through before the next ellipse is drawn.

Launching Other Processes

Win32 processes can launch other processes with the same ease with which they launch threads. The following statements launch Notepad.exe from the Windows directory of drive C:

STARTUPINFO si; ::ZeroMemory (&si, sizeof (STARTUPINFO)); si.cb = sizeof (STARTUPINFO); PROCESS_INFORMATION pi; if (::CreateProcess (NULL, _T ("C:\\Windows\\Notepad"), NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi)) { ::CloseHandle (pi.hThread); ::CloseHandle (pi.hProcess); }

::CreateProcess is a versatile function that takes the name of (and optionally the path to) an executable file and then loads and executes it. If the drive and directory name are omitted from the executable file name, the system automatically searches for the file in the Windows directory, the Windows system directory, all directories in the current path, and in selected other locations. The file name can also include command line parameters, as in

"C:\\Windows\\Notepad C:\\Windows\\Desktop\\Ideas.txt"

::CreateProcess fills a PROCESS_INFORMATION structure with pertinent information about the process, including the process handle (hProcess) and the handle of the process's primary thread (hThread). You should close these handles with ::CloseHandle after the process is started. If you have no further use for the handles, you can close them as soon as ::CreateProcess returns.

A nonzero return from ::CreateProcess means that the process was successfully launched. Win32 processes are launched and executed asynchronously, so ::CreateProcess does not wait until the process has ended to return. If you'd like to launch another process and suspend the current process until the process that it launched terminates, call ::WaitForSingleObject on the process handle, as shown here:

STARTUPINFO si; ::ZeroMemory (&si, sizeof (STARTUPINFO)); si.cb = sizeof (STARTUPINFO); PROCESS_INFORMATION pi; if (::CreateProcess (NULL, _T ("C:\\Windows\\Notepad"), NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi)) { ::CloseHandle (pi.hThread); ::WaitForSingleObject (pi.hProcess, INFINITE); ::CloseHandle (pi.hProcess); }

Processes have exit codes just as threads do. If ::WaitForSingleObject returns anything but WAIT_FAILED, you can call ::GetExitCodeProcess to retrieve the process's exit code.

Sometimes the need arises to launch a process and delay just long enough to make sure the process is started and responding to user input. If process A launches process B and process B creates a window, for example, and process A wants to send that window a message, process A might have to wait for a moment after ::CreateProcess returns to give process B time to create a window and begin processing messages. This problem is easily solved with the Win32 ::WaitForInputIdle function:

STARTUPINFO si; ::ZeroMemory (&si, sizeof (STARTUPINFO)); si.cb = sizeof (STARTUPINFO); PROCESS_INFORMATION pi; if (::CreateProcess (NULL, _T ("C:\\Windows\\Notepad"), NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi)) { ::CloseHandle (pi.hThread); ::WaitForInputIdle (pi.hProcess, INFINITE); // Get B's window handle and send or post a message. ::CloseHandle (pi.hProcess); }

::WaitForInputIdle suspends the current process until the specified process begins processing messages and empties its message queue. I didn't show the code to find the window handle because there isn't a simple MFC or API function you can call to convert a process handle into a window handle. Instead, you must use ::EnumWindows, ::FindWindow, or a related function to search for the window based on some known characteristic of the owning process.

File Change Notifications

Earlier in this chapter, I mentioned that the HANDLE parameter passed to ::WaitForSingleObject can be a "file change notification handle." The Win32 API includes a function named ::FindFirstChangeNotification that returns a handle you can use to wake a blocked thread whenever a change occurs in a specified directory or its subdirectories—for example, when a file is renamed or deleted or a new directory is created.

Let's say you want to enhance Chapter 11's Wanderer application so that changes to the file system are instantly reflected in the left or right pane. The most efficient way to do it is to start a background thread and have it block on one or more file change notification handles. Here's what the thread function for a thread that monitors drive C: might look like:

UINT ThreadFunc (LPVOID pParam) { HWND hwnd = (HWND) pParam; // Window to notify HANDLE hChange = ::FindFirstChangeNotification (_T ("C:\\"), TRUE, FILE_NOTIFY_CHANGE_FILE_NAME ¦ FILE_NOTIFY_CHANGE_DIR_NAME); if (hChange == INVALID_HANDLE_VALUE) { TRACE (_T ("Error: FindFirstChangeNotification failed\n")); return (UINT) -1; } while (...) { ::WaitForSingleObject (hChange, INFINITE); ::PostMessage (hwnd, WM_USER_CHANGE_NOTIFY, 0, 2); ::FindNextChangeNotification (hChange); // Reset } ::FindCloseChangeNotification (hChange); return 0; }

The first parameter passed to ::FindFirstChangeNotification identifies the directory you want to monitor, the second specifies whether you want to monitor just that directory (FALSE) or that directory and all its subdirectories (TRUE), and the third specifies the kinds of changes that the thread should be notified of. In this example, the thread will be awakened when a file is created, renamed, or deleted anywhere on the C: drive (FILE_NOTIFY_CHANGE_FILE_NAME) or when a directory is created, renamed, or deleted (FILE_NOTIFY_CHANGE_DIR_NAME). When the thread is awakened, it posts a user-defined message to the window whose handle was passed in pParam. The message's lParam holds a drive number (2 for drive C:). The window that receives the message—presumably the application's top-level frame window—can respond to the message by updating its views. Keep in mind that a thread awakened by a file change notification doesn't receive any information about the nature of the change or about where in the directory tree the change occurred, so it must scan the file system if it wants to determine what caused the file change notification.

It's also possible to structure the thread so that it monitors not just one drive, but several. All you would have to do is call ::FindFirstChangeNotification once per drive to acquire a separate file change notification handle for each drive and use ::WaitForMultipleObjects to block on all the file change notifications simultaneously. ::WaitForMultipleObjects is the Win32 API equivalent of CMultiLock::Lock. Passing FALSE in the third parameter to a call to ::WaitForMultipleObjects tells the system to wake the thread when any one of the objects that the thread is blocking on becomes signaled.

Категории