July 1998
Download July98Ccode.exe (25KB)
Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and
writer-at-large. He can be reached at askpd@pobox.com
or http://pobox.com/~askpd.
|
Q In the database app I am developing, users can dump records to a text file. There's a modal dialog box in which the user can make some selections and then start the dump by pushing a Start button. This initiates a loop in which records are read from the database and written to the text file. To update the user about the process, I display the current record key in an edit control. I want the user to be able to interrupt the process by pressing a Cancel button, but I can't interrupt the loop this way. How do I implement a Cancel dialog?
Kees Woestenenk
A Whenever your application starts some lengthy operation, it's always a good idea to give the user a way to cancel. Otherwise, it's Ctrl+Alt+Del time and you have a grumpy customer. Given that many apps need a Cancel dialog, you might think it's trivial or at least obvious how to implement one, but it isn't. The problem is, it's fundamentally an asynchronous thing. At any time, a frustrated user might decide that he or she is tired of that operation and wants to bail.The Netherlands Of course, nothing in computing is ever truly asynchronous; it only appears that way. What you have to do is synchronize the asynchronicity. To understand what that means in this particular context, let me take you on a little trip down memory lane, back to the days of Windows® 3.1 and nonpreemptive multitasking. As most of you know, Windows 3.1 is not a true multitasking system. With Win32®, which uses preemptive multitasking, the operating system can interrupt (preempt) your program at any moment to let some other program run. Win32 is not very friendly about when it decides to barge in. Windows 3.1 is much more polite. In Windows 3.1, multitasking is simulated using message queues. If you've only programmed in MFC, you may not even know what a message queue is because MFC hides it deep inside the functions CWinThread::Run and CWinThread::PumpMessage. But inside every Windows-based program, whether or not it is written with MFC, I assure you there is a main loop that goes something like this: |
|
This is the main loop whereby your program gets messages like WM_PAINT and WM_CREATE and then dispatches them to your window's message procand in the case of MFC, from there to your message handler functions like OnPaint and OnCreate. But in Windows 3.1, GetMessage does more than just get the next message. If there's no message waiting for your app, Windows 3.1 takes the opportunity to deliver messages, if any, to other apps. Windows 3.1 simulates multitasking in a nice, orderly fashion through the GetMessage function (and also PeekMessage). Needless to say, it's not a very safe way to multitask, because if a program goes loopythat is, if it enters a long or seemingly eternal loopit will block not only itself, but also all other running apps! In Win32, loopy apps bring themselves to a halt, not the whole system.
You can see right away from this picture that there's a problem if you want to perform a lengthy operation. Printing is the classic example. When an app prints, it usually displays a Cancel dialog so you can abort in case you suddenly realize the printer is in HP mode when you fed it a PostScript file. But how do programs implement such dialogsin Windows 3.1 or Win32? The answer is described in Charles Petzold's still-classic book, Programming Windows, and the technique used for Windows 3.1 is still valid today. There are two pieces. First, the Windows printing mechanism lets you specify an AbortProc. Before printing each page, Windows calls AbortProc to see if it should abort or not. A typical Windows 3.1 AbortProc looks like this (simplified): |
|
Here, bAbort is a global flag that starts out FALSE and gets turned to TRUE when the user Cancelsin which case AbortProc will return FALSE, which tells Windows to abort printing. But look what else AbortProc does: it runs its own little message loop. This gives both your app and others a chance to process messages. Only when there are no more messages waitingeither for your app or any otherdoes the AbortProc relinquish control back to printing.
As for the Cancel dialog itself, it's an ordinary dialog that sets bAbort=TRUE if the user presses the Cancel button. |
|
There's just one trick: the Cancel dialog must be modeless. You must create it by calling ::CreateDialog (in MFC, CDialog::Create) instead of ::DialogBox (CDialog::DoModal). Why? So Windows will run it asynchronously. If you call DialogBox (CDialog::DoModal), control won't return to your app until the user ends the dialog. Definitely not what you want. You want to launch the dialog and then immediately begin your long operation while the dialog continues running; in other words, it must be modeless.
There are three key features in this handshaking between dialog and AbortProc. First, the long operation (printing in this case) must continually call the AbortProc to see if it's time to cancel; second, the AbortProc must run a mini message loop to let your app and others process messages; and the third, Cancel dialog must be modeless so it runs as if it were a separate top-level app. So much for Windows 3.1 and C. What about C++, MFC, and
I wrote a class called CCancelDialog that encapsulates everything I've just described, and an app called CANCEL1 that shows how to use it. When CANCEL1 starts, it displays the dialog shown in Figure 1. When the user presses Begin Dumping, the program starts dumping records. But first, it disables itself and creates a Cancel dialog. |
|
Figure 2 |
|
|
DumpRecord is an extern function that simulates dumping a record by sleeping for half a second. But note how the main loop calls CCancelDlg::Abort before dumping each record to see if the user wants to cancel.
CCancelDlg::Abort is like AbortProc in the Windows 3.1 example, only you don't have to write it because I already did. |
|
Abort calls PeekMessage to see if there's a message waiting; PeekMessage is like GetMessage, only as the name implies it doesn't actually remove the message from the queue. If there's a message waiting, Abort calls AfxGetThread()->PumpMessage, MFC's mother of all message-routing functions. PumpMessage calls GetMessage/DispatchMessage and a whole lot else as well! It does the PreTranslateMessage thing, which in MFC-land includes routing modeless dialog messages. (One little detail I spared you when I described the simplified AbortProc previously is that it must call ::IsDialogMessage to test whether the message is for your modeless dialog. But MFC in its benevolence does that for you, so all you have to do to dispatch a message in MFC is call one function: PumpMessage.) As you may have guessed, m_bAbort is like bAbort, only now instead of a global, it's a member of CCancelDlg, initialized to FALSE and set to TRUE if the user cancels. |
|
This is the kind of function I really like: one line. Whether or not the user cancels, the for loop in CMainDlg::OnBeginDump eventually ends and OnBeginDump cleans up before returning by destroying the Cancel dialog and reenabling itself. Figure 3 shows CCancelDlg, and Figure 4 shows the app that uses it. It's all pretty simple and straightforwardif you remember your Petzold and Windows 3.1! The actual CANCEL1 app doesn't use CCancelDlg directly as in the above snippets. Instead it uses a derived class, CMyCancelDlg, that has a static text control to show progress (see Figure 2). I implemented the progress portion of the Cancel dialog in a separate class to keep CCancelDlg as generic as possible so you can use it in your own apps.
That's the generic Cancel dialog in a nutshell. Of course, in Win32, there is another way to implement a cancel dialogwhich is the perfect segue to the next question.
Q I have an application that does a data search when the user enters a query. The search may take a long time, so I don't want to lock the app. I want the user to be able to continue using the program while the search is going on, with some way to cancel. Should I use the OnIdle mechanism or do I have to use threads? I've read that threads create all sorts of problems.
John Woo
A This is really more or less the same question as the first one, except for one thing: if you want the user to be able to go on using your app while the long operation continues, you must either use idle processing or Win32 threads. It's true that threads are intimidating; multithreaded apps often exhibit complex behavior, including some very hard-to-find, hard-to-fix bugs. Multithreaded programs also often behave differently on multiprocessor machines, so you must always test them on both types. I usually steer people away from multithreading. Too often I see programmers using threads just to prove how smart they are, when the really smart thing would be to redesign your program slightly to avoid threads entirely.
Toronto That said, there are times when multithreading is way cool. One such situation is when you have some time-consuming task to perform that requires little or no user interaction. Typical examples are reading a big file (from disk or the Web), performing a database search, or dumping records as in the previous question. These are the types of situations where you can use threads with a high likelihood of success. Win32 has low-level API functions to create and manipulate threads, but I'll show you a simple solution that uses the MFC wrapper function AfxBeginThread. While AfxBeginThread is easier than calling the bare-bones Win32 API, it still leaves much room for enhancement, so I wrote a little class called CThreadJob (see Figure 5 ) that implements a generic worker thread. It provides functions to Begin and Abort the thread, as well as a way for the thread to report its progress to the main thread. I used CThreadJob to implement CANCEL2 (Figure 6), which implements a cancelable long operation similar to the one in the previous question. The only difference is that in CANCEL2 the Stop button (formerly Cancel) is in the main dialog itself (see Figure 7). |
Figure 7 |
To see how it works, let's pick up the action when the user presses the Begin Dumping button. |
|
CMainDlg first disables the Begin Dumping button (to prevent the user from dumping again), then starts the worker thread m_job, which is an instance of a new class, CDumpRecordsJob, derived from the generic CThreadJob. |
|
CDumpRecordsJob has only one function: DoWork. It inherits everything else it needs from CThreadJob. When CThreadJob::Begin gets control, it first saves its arguments, then calls AfxBeginThread to start a new thread. |
|
CThreadJob calls AfxBeginThread with ThreadProc and a pointer to itself (this) as the start parameter. ThreadProc is the thread. It starts running in the new thread and the thread terminates when it exits. But ThreadProc doesn't actually do anything; it just passes control back to the CThreadJob object's virtual DoWork function. |
|
The upshot is that instead of implementing a thread proc to perform the work of the thread, you now derive your own class from CThreadJob and implement the virtual function DoWork, which now is the thread; DoWork runs in the new thread, and the thread terminates when DoWork returns. Using a class with a virtual member function instead of a top-level function is not only more object-oriented, it also encourages you not to use globals to communicate with your thread. Instead, you can add whatever data members you need to your CThreadJob-derived class.
For CANCEL2, CDumpRecordsJob doesn't need any extra data; all it has is the virtual function DoWork to dump the records. |
|
This is the same basic dump-a-record loop as in CANCEL1 from the previous question, only now instead of calling CCancelDlg::Abort, DoWork tests m_bAbort (inherited from CThreadJob) directly. After dumping each record, DoWork calls CThreadJob::OnProgress to report how many records have been dumped. OnProgress posts a message using whatever window and message ID you passed to Begin. |
|
It's up to you to decide what wp and lp mean for any particular CThreadJob-derived class. For CDumpRecordsJob, I use wp to hold the number of records dumped so far, with 0 meaning finished and -1 specifying that the user aborted. lp is not used. CMainDlg handles the progress messages by displaying an appropriate string in a static text control in the main dialogfor example, "Dumping record 12 of 100" or "Done" (see Figure 8).
The important thing to observe is that CThreadJob:: OnProgress calls PostMessage, not SendMessage. Why? Because I want the code that handles the message (CMainDlg::OnProgress in Figure 6 ) to run in the main thread, not the worker thread. DoWork runs in the worker thread and posts messages to the main thread, which reports the progress to the user. |
Figure 8 Progress Messages |
This is a good paradigm to follow in general whenever you implement worker threads. The worker thread never manipulates the UI (window, dialog, and so on) directly; instead, it posts messages to the main UI thread, which in turn manipulates the display. By separating the UI from all worker threads this way, you avoid a tangle of potential problems with the internal MFC window handle maps (which are thread-specific), not to mention possible window contention problems. You also end up with a worker thread that can run silently (with no UI), which is often useful if you want to run it in batch mode.
So far so good. The only thing left to explain is how the user cancels. Since you're in multithreading land, this is now trivial. There's no message loop to worry about, and no need to call PeekMessage or GetMessage. After you spawn the worker thread (by calling CThreadJob:: Begin), the main thread (dialog) goes on running as normal. When the user presses the Stop button, CMainDlg::OnStop handles it. |
|
CThreadJob::Abort sets m_bAbort = TRUE. The next time DoWork is about to dump another record, it will discover this fact and quit. Control flows out of DoWork, out of the hidden ThreadProc, and the thread terminates. Of course, this only works because my DoWork function periodically checks m_bAbortsomething you must remember to do in your own DoWork function. Since m_bAbort is used in such a trivial way, there's no need for a critical section or anything like that to synchronize its access. No matter how the operating system preempts either thread, m_bAbort is either on or off; there's nothing in between.
Also, please note that CThreadJob::Abort does not call ::TerminateThread, which in the Redmondtonians' own terrifying words "is a dangerous function that should only be used in the most extreme cases." In general, the proper way to terminate a thread is by signalling it a "stop thyself" event and letting it terminate itself gracefully. Since CANCEL2 is so simple, I can get by using a flag (m_bAbort) instead of an event (CEvent). CThreadJob is completely generic. You can use it to implement worker threads in your own app. Just remember to check m_bAbort from time to time (the more often the better) in your DoWork function, and call OnProgress if you want to report your worker thread's progress. CANCEL1 and CANCEL2 illustrate the difference between Windows 3.1 old-style multitasking and Win32 true preemptive multitasking. To see the difference, run CANCEL1, press BeginDumping, and when the Cancel dialog appears, try moving it around with the mouse by dragging the title bar. The first thing you'll notice is that there's a brief delay between the moment you drag the window and when it actually moves. That's because the dialog doesn't get the mouse events until the dump-a-record loop next calls Abort and Abort calls Peek/PumpMessage, which could have up to a half-second delay. Furthermore, as you drag the Cancel dialog around the screen, the progress indicator doesn't change. This isn't because the screen fails to paint, it's because the records have stopped being dumped! As long as there are mouse messages in the queue, CCancelDlg::Abort doesn't return control to the dump-a-record loop. As soon as you let go of the mouse, the progress window continues ticking off with the next record dumped. This illustrates the choppiness of Windows 3.1 multitasking. Compare this with CANCEL2. If you press BeginDumping, then drag the main dialog around, the dragging is smooth. There's no delay. And even as you move the dialog around, the progress window shows the records are still dumping. Win32 slices time into tiny fragments that give the illusion of simultaneous operation. By the way, you can use this drag-the-window test to tell how your favorite app's Cancel dialog is implemented. For example, in Microsoft® Outlook, if you drag the Cancel dialog that appears when you Compact your personal folder file, the jerkiness and cessation of disk activity suggest this dialog is implemented as in CANCEL1. So which should you use to implement a Cancel dialogCCancelDlg or CThreadJob? It depends. If you want to let your users go on using your app while the long operation takes place, you have to use threads; otherwise, CCancelDlg will do. But even for a modal operation, CThreadJob is slightly more professional because of its smoother operation.
Q I have to write an application using the doc/view architecture of MFC so that every document and view instantiated on a File | New command is in a separate thread. How can I do this? Many readers
A This is an example of the kind of multithreaded approach I would say is doomed from the outset. There are two main reasons. First, there's the inherent problem that MFC's internal handle mapsthe tables that link handles (like HWNDs) to C++ objects (like CWnds)are thread-specific. In other words, if a CWnd exists in one thread's map, it doesn't exist in any other. That's why the MFC documentation advises you to use HWNDs instead of CWnd pointers when sharing Windows objects among threads. You can get by using CWnd, and there are clever ways to improve your odds, but I don't want to even discuss them for fear I might encourage you. Another reason a doc/view-per-thread approach is doomed is that there are simply too many pointers shared amongst docs, views, and frames. Each view points to its parent frame and doc, the doc has a list of view pointers as well as pointers to its CDocTemplate, and there are message maps and all sorts of other stuff inherited from CCmdTarget. In general, MFC has tons of spaghetti pointers pointing up the wazoo. It would be almost impossible to track down all these pointers, let alone synchronize their access using critical sections, which would be necessary in a multithreaded setting. In general, whenever you consider using multiple threads, you should strive to limit your design in two basic ways. First, you want as few threads as possible that directly manipulate the UI. I would say only one: the main thread (process). No other thread should manipulate a window or dialog directly. Threads should perform work. If a thread needs to report its progress or update something on the screen, let it post an event or message to the main thread, which can in turn change the window text or draw a smiley face or whatever. Alternatively, the main thread could query its threads about their state in its ON_COMMAND_UPDATE_UI handlers, and update the screen accordingly. One of the reasons I hate the bouncing balls sample program is that it fosters the idea of multiple threads painting a window. It's cute for bouncing balls and screen savers, but it doesn't work for most apps. You're more likely to succeed, particularly in MFC, if you adopt the rule that only one thread manipulates the UI. (Some exceptions I can think of are CAD and page layout programs where painting the screen is so time-consuming you want to do it in an interruptible UI thread.) My second stern rule for happy multithreading is: limit communication among threads. By that I mean limit the number of resources (objects) they share. In CThreadJob, m_bAbort is an example of a shared object because both the worker and main threads use it, potentially simultaneously. But the way m_bAbort is used (it's either on or off) is so trivial, there's no need to synchronize it, no need to worry about deadlock or any of the other multithreading nasties I absolutely guarantee will make you pull your hair out as soon as you start getting too clever. Of course, it's not always possible to have such narrow bandwidth as in CThreadJob, but you should strive for it as much as possible. So the main model for successful multithreading is a single UI thread with multiple worker threads and narrow communication. This may seem limiting, but it's not. On the contrary, by limiting the design, you make life easier, not harder. I don't know exactly what you have in mind for your MDI app, but you can achieve the effect of each doc/view running in a separate thread as follows: implement the entire UI in a single thread, and the entire app as a vanilla MFC MDI app. Then override your CDocument::OnOpenDocument (or perhaps Serialize) function to load the document in a separate thread. OnOpenDocument would set a state indicator to LOADING, launch a thread to load the data, and return immediately. The view would recognize the document's LOADING state and display accordingly. Perhaps it would paint a message like "Still loading..." or draw an empty rectangle where a picture is expected, as Web browsers do. Your doc/view's ON_COMMAND_UPDATE_UI handlers would likely disable many commands if the document weren't loaded yet. When the worker thread finally finishes loading the file, it would post a message to the doc. When it got the message, the doc would set its state to LOADED and call UpdateAllViews, perhaps with a hint parameter to tell the view exactly what's going on. You could post the message as a WM_NOTIFY message to the main window, with the WM_ NOTIFY handler implemented in your document class. You could even post partial progress events as the document is loaded. The view would have to know how to display partial results the way most Web browsers know how to display partial GIF and JPG files. In fact, most Web browsers work exactly as I am describing, as do URL monikers and CAsyncMonikerFile, which have callback interfaces to report OnProgress and OnDataAvailable. If your app has other long operations such as lengthy calculations, you can implement worker threads for those too. If you take this approach, you'll end up with an app that appears to run each MDI child in a separate thread, when in fact there is one main UI thread communicating in a very limited manner with many worker threads. Happy multithreading! |
|
From the July 1998 issue of Microsoft Systems Journal.