Any non-trivial graphical application has to perform long operations such as reading and saving large files from disk, accessing the network, and carrying out expensive computations. However, implementing long operations without taking care of multithreading would utterly jeopardize user satisfaction.
Indeed, the graphical subsystem of Windows (on the top of which both .NET WinForms and WPF are built) is intrinsically single-threaded. It is based on a message queue where individual actions (processing a button click or rendering the dialog box) are executed one after the other. Therefore, when a button event handler executes, the progress bar cannot be rendered, even if its value has been updated. Neither can the user interface react properly to a click of the Cancel button.
Consequently, a golden rule for graphical programming is to never do anything long in the GUI thread. A few dozen milliseconds is the maximum we can afford to block the message queue, if we want users to be satisfied. And we do want this satisfaction.
The commonly used solution is to execute long operations in a worker thread. In the .NET Framework, it is generally considered best practice not to create a new thread for every operation, but instead, to queue a work item into the thread pool. This operation used to be difficult, but C# 2.0 and anonymous methods have fortunately made it easier.
The following piece of code handles clicks by using the Save button. It queues the I/O operation for asynchronous execution on a worker thread.
private void OnApplyClick(object sender, RoutedEventArgs e) { ThreadPool.QueueUserWorkItem( () => this.contact.Save() ); }
Now, what if we want to display a message after the contact has been saved? Since the graphical subsystem is single-thread, we cannot invoke the MessageBox.Show method from the worker thread. Therefore, we have to dispatch it to the GUI thread. With WPF, we have to use a dispatcher object, as demonstrated in the following code sample:
private void OnApplyClick(object sender, RoutedEventArgs e) { ThreadPool.QueueUserWorkItem( delegate { this.contact.Save(); this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action( () => MessageBox.Show(Window.GetWindow(this),"Contact Saved!") )); }); }
As can be seen, multithreading quickly makes the code unreadable and error-prone. Fortunately, there is a superior solution. What if we have the possibility to mark the affinity of methods directly to the worker thread, or GUI thread, and eliminate the plumbing code? It seems unrealistic, doesn’t it? But with PostSharp and aspect-oriented programming, it is not.
So let’s dream on. What we want are two custom attributes: the OnWorkerThreadAttribute, when the method should be executed asynchronously on a worker thread; and the OnGuiThreadAttribute, when the method should be executed on the GUI thread.
The above piece of code would look like this:
[OnWorkerThread] private void OnApplyClick(object sender, RoutedEventArgs e) { this.contact.Save(); this.ShowMessage("Contact Saved!"); } [OnGuiThread] private void ShowMessage(string message) { MessageBox.Show(Window.GetWindow(this), message); }
Implementing the OnWorkerThread and OnGuiThread Attributes
If you are not already familiar with PostSharp, you may find it strange that custom attributes can actually change the behavior of methods. Indeed, these new custom attributes will have the effect of modifying the methods to which they are applied. But PostSharp is not an ordinary library: it is a tool that inserts itself in the build process and enhances the assemblies after the compiler did its job.
So the first thing to do is to download PostSharp and install it. Next, add PostSharp.dll to your project references.
We are now ready to develop our two aspects.
Both custom attributes will be derived from the class PostSharp.Aspects.MethodInterceptionAspect. They will intercept calls to the method to which they are applied.
Our first custom attribute, OnWorkerThreadAttribute, is trivial:
using System; using System.Threading; using PostSharp.Aspects; namespace ContactManager.Aspects { [Serializable] public class WorkerThreadAttribute : MethodInterceptionAspect { public override void OnInvoke(MethodInterceptionArgs args) { ThreadPool.QueueUserWorkItem( args.Proceed ); } } }
In this attribute, we implemented the method OnInvoke instead of the intercepted method.
The statement args.Proceed() then proceeds with the invocation of the intercepted method. As can be seen, the implementation of this aspect simply queues the execution of the intercepted method into the thread pool.
The implementation of the OnGuiThreadAttribute is a little more complex, because we first need to check if we are already on the GUI thread. If we are not, we need to invoke the intercepted method through Dispatcher.Invoke.
using System.Windows.Threading; using PostSharp.Aspects; namespace ContactManager.Aspects { [Serializable] public class OnGuiThreadAttribute : MethodInterceptionAspect { public DispatcherPriority Priority { get; set; } public override void OnInvoke( MethodInterceptionArgs args) { DispatcherObject dispatcherObject = (DispatcherObject) args.Instance; if (dispatcherObject.CheckAccess()) { // We are already in the GUI thread. Proceed. args.Proceed(); } else { // Invoke the target method synchronously. dispatcherObject.Dispatcher.Invoke(this.Priority, new Action(args.Proceed)); } } } }
The args parameter contains everything we need to know about the intercepted method. Here, we are interested in the target instance of the method, because we need to cast it and retrieve its dispatcher.
These two very simple custom attributes can have a significant impact on the way you think about multithreading. You can now forget about the thread pool and the WPF message dispatcher. All you have to think about is where the method should be executed: can it run asynchronously on a worked thread, or does it require a GUI thread? Using these two simple aspects makes your code easier to read and less error-prone.
Putting operations on threads is only one side of the equation, and arguably the easiest one. What is much more difficult, is to avoid conflicts when accessing resources that are shared by different threads. This is what I will address in the second part.