Keeping an application’s UI responsive with multi-threading

This is not a new topic to be sure, but application responsiveness is a fundamental issue that continues to plague many applications running on the Windows platform. As a developer, I encounter these issues and to some degree I at least understand what is happening and that generally it is the design of the application that is causing them. For the average software consumer however, poor software design leads to frustration and perhaps the perception that their system is somehow to blame.

There is a pretty broad spectrum of non-responsiveness, ranging from jarring or delayed user interface updates to a complete lack of accepting user input where an application’s interface becomes ghosted and shows as “not responding” in Task Manager. Avoiding these problems is conceptually pretty straight forward: separate UI blocking work onto a worker thread. In a Windows application, the user interface thread is the primary thread of the process. This thread is what executes WinMain and processes the message pump for the application’s main window. If some lengthy operation is executing on this thread, no messages are being pumped and the main window becomes non-responsive. The term lengthy is somewhat subjective. Even a short operation can cause responsiveness issues, especially when you consider the cumulative effect of multiple serially executed short operations.

I like to break down the types of operations appropriate for worker threads into two major categories: active and passive. Passive operations are those that are generally transparent to the application user. The user is not necessarily aware that they are occurring. An example of this might be a file browser extracting file properties or creating thumbnail images. Active operations are those that are explicitly invoked by the user. I push this big button and I know that I will be reticulating splines for a while. Both types require some thought about user interface design and the design of the program code. For example, take the file browsing case where there is a list of files displayed. As the user selects different files, the details of those files should be displayed (a passive operation). If the selection is rapidly changing, the details extraction for a previously selected file may still be in progress but the results of that extraction are no longer relevant. This requires your application to be resilient to these types of interactions, perhaps by making the worker threads fire-and-forget or by making them cancellable. For active operations the design should generally allow them to be explicitly cancellable and the interface should provide feedback to the user about their progress.

So how do you as a developer identify when you should be delegating to a worker thread? Well, there are the obvious cases like I called out above, but there are also less obvious cases. Perhaps you are making some Windows API call that seems benign when in fact somewhere down the stack it invokes some I/O. There are tools that can help. The profiler built in to Visual Studio is one possibility, and since performance is such a fundamental issue Microsoft also provides a great toolset specifically for analyzing it at the Windows Performance Analysis Developer Center. In addition you can download the Windows symbols using the Debugging Tools for Windows and the Microsoft symbol server. In many cases, being able to see deep call stacks resulting from a Windows API call can help identify potential candidates for multi-threading.

To help design responsive applications, the Windows API includes a rich surface of multi-threaded APIs and services. Scenarios that I described above as passive are in some cases ideal candidates for the Windows thread pool services. Using the Windows thread pool reduces the amount of thread management required by your app and allows the system to schedule and optimize the execution of your work items. If the thread pool does not meet your needs or perhaps you have requirements that are incompatible with it (for example if need to consume STA COM objects) there are still the base multi-threading APIs and synchronization primitives and wait functions available.

Designing a well behaved app that correctly utilizes worker threads is not trivial, but it is also far from unmanageable. In the end, your users will appreciate the effort even though ironically they may not even be aware of what you have done.


aggbug.aspx

More...
 
Back
Top