This article will introduce a convenient class to handle situations where long procedures must be handled without causing your application to freeze. It is well suited when implementing a thread is not the most desired approach to the problem.
About 8 years ago, when trying to develop a simulator application for Windows, one of the hurdles we had to deal with was long processes. It was no problem creating methods to do the processing, but once you entered the loop, that was it, and we were stuck for the duration. If this was anything more than a few seconds it quickly became a problem, and users would hopelessly tap on the keys or click the mouse in an attempt to abort. At the time, threads were not an option in the design, so another answer had to be found.
When handling long processes, most often they are the result of intensive looping, and often loops within loops. Once you enter the loop, you don't exit the loop until the entire process is finished. Sometime this process takes minutes or even hours to complete. During the whole time, no messages can be processed by the application and window buttons, controls, toolbars and keys become quite useless. To abort, you need to process the message from one of these, and that requires that at some time the application must return to the message loop.
In the early development work, we developed everything directly with the Windows API. This meant that we also had to create the message loop. Since we had to return to the message loop, the answer lay in the loop itself. Rather than creating a large outer loop to the process, the natural looping provided by the messages was used to provide support for the long procedure. All that was needed was to hook into it and perform a loop test to decide when we were finished. C++ application objects were written to manage the message loop and its loop task processing. Smaller objects called 'tasks' were created to hook into the loop whenever it was needed.
When MFC came along, the application class was discovered to provide the same basic support in the virtual method called
OnIdle(). All that was now required was to keep the hook code and leave the MFC message loop undisturbed. This was particularly attractive because it meant that we could use virtually the same code when the application was recently ported to use MFC. In addition, since we did not resort to multiple message processing points, all the window messaging could remain under the control of MFC.
Even though an idle processing call is provided, its not very convenient. There is no nice way to hook into it and detach from it. What is needed is a structure and a simple object to allow the developer to create sets of code that can be added and removed in a convenient fashion. This way, blocks of execution can be 'packaged' up, processed with a clean interface that is not unlike a thread object. The class that was created for this purpose is called
ZTask. Everything in our libraries is prefixed by a Z but you can change it to anything you like. This 'task' object forms the basis of this discussion.
To use the idle processing class, all that is required is a simple change to the derived
CWinApp class (In this case called
MyApp). Once all the tasks have indicated their completion, this method will return false and cause the application to sleep until the next message arrives. This is the only change required in the application to enable task processing.
BOOL MyApp::OnIdle(LONG count)
return CWinApp::OnIdle(count) || ZTask::executeTasks();
Once tasks are added to the application, through specific methods in the source code, the application then managed the objects automatically. The tasks perform a single pass for each idle call and continue until all the tasks are complete. All that is required is to create these objects to do you work and get them started by adding them to the application.
The tasking objects
Now that we have support in the application, defining the work to be done was clean and easy. A tasking object called
ZTask was created to package up individual tasks into nice bundles. The class has virtual methods that are called when it is first added, each time an idle call is made, and one when it is removed. The method for processing a single iteration of the task is called
execute(). It is pure and must be overridden when in the derived task. The other two are optional. The
execute method will continue to be called in the idle loop until you return true. Once true is returned the task object will be cleaned up and deleted if necessary. All you need to do is decide how much processing you want to perform in a single pass. This will affect the responsiveness of the application.
virtual bool execute() = 0;
Since tasks are executed in the idle processing of the primary thread of the application, it is not predictive when the task will be done. If the task is created on the heap using
new then it must also be deleted. Like threads, the task object has a flag that can be set to tell it to delete itself automatically when the task is done, so you don't have to.
Why not threads?
- Often program code does not need to run in complete autonomous thread. Rather, what is needed is some means to simply 'defer' the execution until the application is less busy. This keeps the primary thread running crisp and responsive.
- It is very often the case that code must run a certain section without intervention (like a CRITICAL section in a thread). Since tasks are running in the primary thread, there is no intervention and you do not need to define these locks.
- If you need access to resources or need to pain, you can do it in a thread, but in MFC it is a messy affair since you must attach and detach windows and resources to keep the internal tables in check.
One-Shots are pieces of code that need to be executed asynchronously, but only require a single pass. This is typically what happens when messages are processed such as painting or button clicks. A task object can also be used in exactly this fashion. The big advantage here is all the code associated with the task can be packaged together rather than distributed through window messages. All the task does is return true on the first pass. It is then removed right away by the application object. If the task is embedded as a member of another class, it is not auto-deleted and can be reused over and over by simply adding back to the application whenever needed.
In our applications, one-shot tasks are used extensively as update objects for trees and backing stores for controls and graphs. We do not want these activities to consume CPU time until the message queue is quiet, thus retaining responsiveness in the application. Since potentially dozens of these objects may be present on the display surface, interaction with them is triggering many update tasks that are queued until the application is idle. These objects are part of a library of tools and so they must be able to add the tasks automatically as needed, and without additional coding.
Using the objects depends a lot on your needs. If you want to invoke a task more than once, as in the case of an updater, you may want to construct the object embedded with the auto-delete flag set to false. When you need to start it, you may make a call like this:
When it has completed (by returning true) it will automatically be removed again. We use this strategy to update backing stores for components. If you want to create a task and then forget about it, you would likely create it dynamically and then start it right away. Auto-delete would be turned on in this situation so that the object is cleaned up once it is finished.
One of the benefits I have found with this approach is, it never runs while a window is scrolling, so it will not interfere with user interaction.
This class utilized a
Vector class that is described in another article that I have already posted. You can easily replace this with a
CTypedPtrList if you do not wish to use the
Vector class. We utilize our own collection classes for maintenance reasons. The code provided here is well documented and should be easy to follow.