Safe, Simple Multithreading in Windows Forms, Part 3
Chris Sells
Sells Brothers Consulting
January 23, 2003
Summary: Chris Sells discusses how to more cleanly communicate between a UI thread and a worker thread, and how to call Web services asynchronously in a Windows Forms application. (11 printed pages)
Download the winforms01232003.exe sample file.
I was once described in a conference talk as "pedantic." Since taking the Webster's Dictionary definition to heart can only lead to hurt feelings, I prefer my friend's definition, who says that I'm "like a dog with a bone." This latter definition I take as a complement because I do like to make sure that I do a job all the way. In this case, I found that I still had more to say on the topic of Windows Forms and multithreading, both as to how to more cleanly communicate between a UI thread and a worker thread, and how to call Web services asynchronously in a Windows Forms application.
Communication with Message Passing
Figure 1. Calculating pi
Recall from Safe, Simple Multithreading in Windows Forms, Part 2 that we had built a client for calculating pi to an arbitrary number of digits on a worker thread, leaving us with the following function to communicate results from the worker thread to the UI thread:
void ShowProgress(..., out bool cancel) { if( _pi.InvokeRequired == false ) { ... } // Transfer control to UI thread else { ShowProgressDelegate showProgress = new ShowProgressDelegate(ShowProgress); // Avoid boxing and losing our return value object inoutCancel = false; // Show progress synchronously (so we can check for cancel) Invoke(showProgress, new object[] { ..., inoutCancel}); cancel = (bool)inoutCancel; } }
Remember that we were using an out parameter to communicate back from the UI thread, regardless of whether the user had canceled the operation or not. Because the out parameter was a value type instead of a reference type, we had to write some pretty unpleasant code to avoid updating a boxed copy in the UI thread and losing that information in the worker thread. The simplicity of the CalcPi example, and the resulting complexity of sending around a single Boolean indicating whether to cancel or not, may cause us to try a solution like the following:
void ShowProgress(..., out bool cancel) { // Make sure we're on the right thread if( this.InvokeRequired == false ) {...} // Transfer control to correct thread else { ShowProgressDelegate showProgress = new ShowProgressDelegate(ShowProgress); // Show progress synchronously (so we can check for cancel) Invoke(showProgress, new object[] {...}); // Check for Cancel the easy, but special-purpose, way cancel = (state == CalcState.Canceled); } }
For our simple application, and others like it, that would work just fine. Since the worker thread only reads from the state field, it will always be valid (although a race condition could cause it to be out of date). However, don't be tempted down this path. As soon as we have multiple outstanding requests and we keep them in an array or any kind of data structure, we run the risk of attempting to access data that has been invalidated (those darn race conditions again), which we'll have to protect against using synchronization (remember deadlocks?). It's so much simpler and safer to pass around ownership of the data instead of sharing access to the same data. If the weird boxing thing required by the Control.Invoke call turns you off, I recommend following the delegate pattern used by the rest of the .NET Framework:
class ShowProgressArgs : EventArgs { public string Pi; public int TotalDigits; public int DigitsSoFar; public bool Cancel; public ShowProgressArgs( string pi, int totalDigits, int digitsSoFar) { this.Pi = pi; this.TotalDigits = totalDigits; this.DigitsSoFar = digitsSoFar; } } delegate void ShowProgressHandler(object sender, ShowProgressArgs e);
This code declares the class ShowProgressArgs that derives from the EventArgs base class to hold event arguments, as well as a delegate that takes a sender and an instance on the custom arguments object. With this in place, we can change the ShowProgress implementation like so:
void ShowProgress(...) { // Make sure we're on the right thread if( this.InvokeRequired == false ) {...} } // Transfer control to correct thread else { ShowProgressHandler showProgress = new ShowProgressHandler(AsynchCalcPiForm_ShowProgress); object sender = System.Threading.Thread.CurrentThread; ShowProgressArgs e = new ShowProgressArgs(pi, totalDigits, digitsSoFar); Invoke(showProgress, new object[] {sender, e}); cancel = e.Cancel; } } void AsynchCalcPiForm_ShowProgress(object sender, ShowProgressArgs e) { ShowProgress(e.Pi, e.TotalDigits, e.DigitsSoFar, out e.Cancel); }
ShowProgress hasn't changed in signature, so CalcPi still calls it in the same simple way, but now the worker thread will compose an instance of the ShowProgressArgs object to pass the appropriate arguments to the UI thread through a handler that looks like any other event handler, including a sender and an EventArgs-derived object. It turns around and calls the ShowProgress method again, breaking out the arguments from the ShowProgressArgs object. After Invoke returns in the worker thread, it pulls out the cancel flag without any concern about boxing because the ShowProgressArgs type is a reference type. However, even though it is a reference type and the worker thread passes control of it to the UI thread, there's no danger of race conditions because the worker thread waits 'til the UI thread is done working with the data before accessing it again.
This usage could be further simplified if we want the CalcPi function to create instances of ShowProgressArgs itself, eliminating the need for an intermediate method:
void ShowProgress(object sender, ShowProgressArgs e) { // Make sure we're on the right thread if( this.InvokeRequired == false ) { piTextBox.Text = e.Pi; piProgressBar.Maximum = e.TotalDigits; piProgressBar.Value = e.DigitsSoFar; // Check for Cancel e.Cancel = (state == CalcState.Canceled); // Check for completion if( e.Cancel || (e.DigitsSoFar == e.TotalDigits) ) { state = CalcState.Pending; calcButton.Text = "Calc"; calcButton.Enabled = true; } } // Transfer control to correct thread else { ShowProgressHandler showProgress = new ShowProgressHandler(ShowProgress); Invoke(showProgress, new object[] { sender, e}); } } void CalcPi(int digits) { StringBuilder pi = new StringBuilder("3", digits + 2); object sender = System.Threading.Thread.CurrentThread; ShowProgressArgs e = new ShowProgressArgs(pi.ToString(), digits, 0); // Show progress (ignoring Cancel so soon) ShowProgress(sender, e); (checking for Cancel) e.Pi = pi.ToString(); e.DigitsSoFar = i + digitCount; ShowProgress(sender, e); if( e.Cancel ) break; } } }
This technique represents a message passing model. This model is clear, safe, and general-purpose. It's clear because it's easy to see that the worker is creating a message, passing it to the UI, and then checking the message for extra information additional information besides a cancel flag, that information can be added to the ShowProgressArgs class. Message passing is what our applications should be using to communicate between UI and working threads.
Asynchronous Web Services
One specific area in which we'll want to use Windows Forms applications in an asynchronous manner is Web services. Web services are like passing messages between threads except that the messages travel between machines using standard protocols, such as HTTP and XML. Imagine a .NET Framework-based Web service that calculated digits of pi using some way-cool, fast pi calculation engine:
Now imagine a version of the CalcPi program that used the Web service instead of our slow client-side algorithm to calculate pi on giant machines with huge processors (or even better, databases with more digits of pi cached than anyone could ever want or need). Although the underlying protocols of Web services are HTTP- and XML-based, and we could form a Web service request fairly easily to ask for the digits of pi we're after, it's even simpler to use the client-side Web services proxy generation built into Visual Studio® .NET in the Project menu with the Add Web Reference item. The resultant Add Web Service dialog allows us to enter the URL of the WSDL (Web Service Description Language) that describes the Web service we'd like to call; for example. In this case some code will be added to our project from. To see it, we'll need to look in the Solution Explorer under the <SolutionName>/<ProjectName>/Web References/<ServerName>/Reference.map/Reference.cs. The generated proxy code for the CalcPi Web service looks like this:
namespace AsynchCalcPi.localhost { [WebServiceBindingAttribute(Name="CalcPiServiceSoap", ...)] public class CalcPiService : SoapHttpClientProtocol { public CalcPiService() { this.Url = ""; } [SoapDocumentMethodAttribute("", ...)] public string CalcPi(int digits) {...} public IAsyncResult BeginCalcPi( int digits, System.AsyncCallback callback, object asyncState) {...} public string EndCalcPi(IAsyncResult asyncResult) {...} } }
Calling the Web service is now a matter of creating and instance of the proxy and calling the method we're interested in:
As with other kinds of long-running methods, if this one takes a long time it'll block the UI thread. The standard techniques already discussed in this series can be used to call synchronous Web service methods asynchronously, but as you can tell in the generated proxy code, there's already built-in support for asynchronous operations through the BeginXxx/EndXxx method pairs, one for each method on the Web service.
Retrofitting our existing CalcPi client to use the Web service begins by calling the Web service's CalcPi instead of our own:
enum CalcState { Pending, Calculating, Canceled, } CalcState state = CalcState.Pending; localhost.CalcPiService service = new localhost.CalcPiService(); void calcButton_Click(object sender, System.EventArgs e) { switch( state ) { case CalcState.Pending: state = CalcState.Calculating; calcButton.Text = "Cancel"; // Start web service request service.BeginCalcPi( (int)digitsUpDown.Value, new AsyncCallback(PiCalculated), null); break; case CalcState.Calculating: state = CalcState.Canceled; calcButton.Enabled = false; service.Abort(); // Fail all outstanding requests break; case CalcState.Canceled: Debug.Assert(false); break; } } void PiCalculated(IAsyncResult res) {...}
This time, instead of declaring a custom delegate to start the asynchronous operation, BeginCalcPi takes, in addition to the method parameters, an instance of an AsyncCallback delegate, which returns void and takes an IAsyncResult as the sole argument. The application provides the PiCalculated method that matches that signature. The PiCalculated method will be called when the Web service returns and is responsible for harvesting results.
Also, while there is no way to get progress from a Web service (all the more reason to call it asynchronously), calls to a Web service can be canceled by calling the Abort method. Be careful with this one, however, as it will cancel all outstanding requests, not just a specific one.
When the PiCalculated method is called, that means that the Web service has returned something:
The call to EndCalcPi, passing in the IAsyncResult parameter, provides whatever results we've gotten by calling the Web service. If the Web service call was successful, pi is the returned value. If, on the other hand, the Web service call was unsuccessful because it timed out or was canceled, EndCalcPi will thrown a WebException error, which is why PiCalculated wraps the call to EndCalcPi in a try-catch block. ShowPi will be called on to either show the digits of pi that were calculated by the Web service or the exception message (the result of a canceled Web service call is shown in Figure 2).
Figure 2. The result of a canceled call to the CalcPi Web service
delegate void ShowPiDelegate(string pi); void ShowPi(string pi) { if( this.InvokeRequired == false ) { piTextBox.Text = pi; state = CalcState.Pending; calcButton.Text = "Calc"; calcButton.Enabled = true; } else { ShowPiDelegate showPi = new ShowPiDelegate(ShowPi); this.BeginInvoke(showPi, new object[] {pi}); } }
Notice that the ShowPi method contains the check to see if an invoke is required and makes a call to BeginInvoke to transfer from the current thread to the UI thread. This is necessary because the Web service completion notification will come in on some thread other than the UI thread, and by now we know how important it is to translate the call back to the UI thread before touching any of the controls.
Where Are We?
Just when you thought it was safe to eat pie again, I had more things to say about how to handle multithreaded operations in Windows Forms cleanly and safely. Not only does this simple example leverage multiple threads to split the UI from a long-running operation, but also the UI thread communicates further user input back to the worker thread to adjust its behavior. While it could have used shared data, the application used a message-passing scheme between threads to avoid the complications of synchronization. Finally, the application was further extended to take advantage of asynchronous calls to Web services, which is really the only way to call them as even quick methods can take arbitrary long amounts of time across the Web..