Safe, simple multithreading in Windows Forms, part 1

Source: Internet
Author: User
Safe, simple multithreading in Windows Forms, part 1

Chris sells

June 28,200 2

Download the asynchcalcpi.exe sample.

It all started innocently enough. I found myself needing to calculate the area of a circle for the first time in. net. this called, of course, for an accurate representation of pi. system. math. pi is handy, but since it only provides 20 digits of precision, I was worried about the accuracy of my calculation (I really needed 21 digits to be absolutely comfortable ). so, like any programmer worth their salt, I forgot about the problem I was actually trying to solve and I wrote myself a program to calculate pi to any number of digits that I felt like. what I came up with is shown in Figure 1.

Figure 1. digits of PI Application

Progress on long-running operations

While most applications don't need to calculate digits of Pi, runtime kinds of applications need to perform long-running operations, whether it's printing, making a web service call, or calculating interest earnings on a certain billionaire in the Pacific Northwest. users are generally content to wait for such things, often moving to something else in the meantime, so long as they can see that progress is being made. that's why even my little application has a progress bar. the algorithm I'm using calculates PI nine digits at a time. as each new set of digits are available, my program keeps the text updated and moves the progress bar to show how we're re coming along. for example, Figure 2 shows progress on the way to calculating 1000 digits of PI (if 21 digits are good, than 1000 must be better ).

Figure 2. calculating pi to 1000 digits

The following shows how the user interface (UI) is updated as the digits of Pi are calculated:

 
Void showprogress (string Pi, int totaldigits, int digitssofar) {_ pI. Text = PI; _ piprogress. Maximum = totaldigits; _ piprogress. value = digitssofar ;}Void calcpi (INT digits) {stringbuilder Pi = new stringbuilder ("3", digits + 2 );// Show progressShowprogress (PI. tostring (), digits, 0 );If (digits> 0) {pi. append (". "); For (INT I = 0; I <digits; I + = 9) {int ninedigits = ninedigitsofpi. startingat (I + 1); int digitcount = math. min (digits-I, 9); string DS = string. format ("{0: D9}", ninedigits); Pi. append (Ds. substring (0, digitcount ));// Show progressShowprogress (PI. tostring (), digits, I + digitcount );}}}

Everything was going along fine until, in the middle of actually calculating pi to 1000 digits, I switched away to do something else and then switched back. What I saw is shown in figure 3.

Figure 3. No paint event for you!

the problem, of course, is that my application is single-threaded, so while the thread is calculating pi, it can't also be drawing the UI. I didn't run into this before because when I set the textbox. text and progressbar. value properties, those controls wocould force their painting to happen immediately as part of setting the property (although I noticed that the progress bar was better at this than the text box ). however, once I put the application into the background and then the foreground again, I need to paint the entire client area, and that's a paint event for the form. since no other event is going to be processed until we return from the event we're already processing (that is, the click event on the calc button ), we're out of luck in terms of seeing any further progress. what I really needed to do was free the UI thread for doing UI work and handle the long-running process in the background. for this, I need another thread.

Asynchronous operations

My current synchronousClickHandler looked like this:

 
Void _ calcbutton_click (Object sender, eventargs e) {calcpi (INT) _ digits. Value );}

Recall that the issue isCalcpiReturns, the thread can't return from ourClickHandler, which means the form can't handlePaintEvent (or any other event, for that matter). One way to handle this is to start another thread, like so:

 
Using system. Threading; ã ‚ â... Int _ digitstocalc = 0; void calcpithreadstart () {calcpi (_ digitstocalc);} void _ calcbutton_click (Object sender, eventargs e) {_ digitstocalc = (INT) _ digits. value;Thread pithread = new thread (New threadstart (calcpithreadstart ));Pithread. Start ();}

Now, instead of waitingCalcpiTo finish before returning from the buttonClickEvent, I'm creating a new thread and asking it to start.Thread. StartMethod will schedule my new thread as ready to start and then return immediately, allowing our UI thread to get back to its own work. now, if the user wants to interact with the application (put it in the background, move it to the foreground, resize it, or even close it ), the UI thread is free to handle all of those events while the worker thread calculates Pi at its own pace. figure 4 shows the two threads doing the work.

Figure 4. Naive Multithreading

You may have noticed that I'm not passing any arguments to the worker thread's entry point-Calcpithreadstart. Instead, I'm tucking the number of digits to calculate into a field,_ Digitstocalc, Calling the thread entry point, which is callingCalcpiIn turn. this is kind of a pain, which is one of the reasons that I prefer delegates for asynchronous work. delegates support taking arguments, which saves me the hassystemic of an extra temporary field and an extra function between the functions I want to call.

If you're not familiar with delegates, they're really just objects that call static or Instance functions. in C #, they're declared using function declaration syntax. for example, a delegate to callCalcpiLooks like this:

 
Delegate void calcpidelegate (INT digits );

Once I have a delegate, I can create an instance to callCalcpiFunction synchronously like so:

 
Void _ calcbutton_click (Object sender, eventargs e ){Calcpidelegate calcpi = new calcpidelegate (calcpi );Calcpi (INT) _ digits. value );}

Of course, I don't want to callCalcpiSynchronously; I want to call it asynchronously. Before I do that, however, we need to understand a bit more about how delegates work. My delegate Declaration above declares a new class derived fromMulticastdelegateWith three functions,Invoke,Begininvoke, AndEndinvoke, As shown here:

ClassCalcpidelegate: Multicastdelegate {Public void invoke (INT digits );Public void begininvoke (INT digits, asynccallback callback,Object asyncstate );Public void endinvoke (iasyncresult result );}

When I created an instance ofCalcpidelegateEarlier and then called it like a function, I was actually calling the synchronousInvokeFunction, which in turn called my ownCalcpiFunction.BegininvokeAndEndinvoke, However, are the pair of functions that allow you to invoke and harvest the results of a function call asynchronously. So, to haveCalcpiFunction called on another thread, I need to callBegininvokeLike so:

Void _ calcbutton_click (Object sender, eventargs e) {calcpidelegate calcpi = new calcpidelegate (calcpi );Calcpi. begininvoke (INT) _ digits. Value, null, null );}

Notice that we're passing nulls for the last two argumentsBegininvoke. These are needed if we 'd like to harvest the result from the function we're calling at some later date (which is also whatEndinvokeIs For). SinceCalcpiFunction updates the UI directly, we don't need anything but nulls for these two arguments. if you 'd like the details of delegates, both synchronous and asynchronous, see. net delegates: a c # bedtime story.

At this point, I shoshould be happy. I 've got my application to combine a fully interactive UI that shows progress on a long-running operation. in fact, it wasn' t until I realized what I was really doing that I became unhappy.

Multithreaded safety

As it turned out, I had just gotten lucky (or unlucky, depending on how you characterize such things ). microsoft Windows XP was providing me with a very robust implementation of the underlying operating system on which Windows Forms is built. so robust, in fact, that it gracefully handled my violation of the Prime Directive of Windows Programming- Though shalt not operate on a window from other than its creating thread . unfortunately there's no guarantee that other, less robust implementations of Windows wocould be equally graceful given my bad manners.

the problem, of course, was of my own making. if you remember figure 4, I had two threads accessing the same underlying window at the same time. however, because long-running operations are so common in Windows application, each UI class in Windows Forms (that is, every class that ultimately derives from system. windows. forms. control) has a property that you can use from any thread so that you can access the window safely. the name of the property is invokerequired , which returns true if the calling thread needs to pass control over to the creating thread before calling a method on that object. A simple assert in my showprogress function wocould have immediately shown me the error of my ways:

 using system. diagnostics; void showprogress (string Pi, int totaldigits, int digitssofar) {  // make sure we're on the right thread    debug. assert (_ PI. invokerequired = false);   ...} 

In fact,. net documentation is quite clear on this point. it states, "there are four methods on a control that are safe to call from any thread: invoke , begininvoke , endinvoke , and creategraphics . for all other method CILS, you shoshould use one of the invoke methods to invoke al the call to the control's thread. "So, when I set the control properties, I'm clearly violating this rule. and from the names of the first three functions that I'm allowed to call safely ( invoke , begininvoke , and endinvoke ), It shoshould be clear that I need to construct another delegate that will be executed in the UI thread. if I were worried about blocking my worker thread, like I was worried about blocking my UI thread, I 'd need to use the asynchronous begininvoke and endinvoke . however, since my worker thread exists only to service my UI thread, let's use the simpler, synchronous invoke method, which is defined like this:

Public object invoke (delegate method); Public object invoke (delegate method, object [] ARGs );

The first overloadInvokeTakes an instance of a delegate containing the method we 'd like to call in the UI thread, but assumes no arguments. However, the function we want to call to update the UI,Showprogress, Takes three arguments, so we'll need the second overload. We'll also need another delegate for ourShowprogressMethod so that we can pass the arguments correctly. Here's how to useInvokeTo make sure that our calltoShowprogress, And therefore our use of our windows, shows up on the correct thread (making sure to replace both calltoShowprogressInCalcpi):

   delegatevoid showprogressdelegate (string Pi, int totaldigits, int digitssofar );   void calcpi (INT digits) {stringbuilder Pi = new stringbuilder ("3", digits + 2 );   // get ready to show progress asynchronously    showprogressdelegate showprogress =    New showprogressdelegate (showprogress );    // show progress    This. invoke (showprogress, new object [] {pi. tostring (), digits, 0});   If (digits> 0) {pi. append (". "); For (INT I = 0; I 
   
    // show progress   
    This. invoke (showprogress,   
    new object [] {pi. tostring (), digits, I + digitcount});  }}
  

The useInvokeHas finally given me a safe use of multithreading in my Windows Forms Application. the UI thread spawns a worker thread to do the long-running operation, and the worker thread passes control back to the UI thread when the UI needs updating. figure 5 shows our safe multithreading architecture.

Figure 5. Safe Multithreading

Simplified Multithreading

The callInvokeIs a bit cumbersome, and because it happens twice in ourCalcpiFunction, We cocould simplify things and updateShowprogressItself to do the asynchronous call. IfShowprogressIs called from the correct thread, it will update the controls, but if it's called from the incorrect thread, it usesInvokeTo call itself back on the correct thread. This lets us go back to the previous, simplerCalcpi:

Void showprogress (string Pi, int totaldigits, int digitssofar ){ // Make sure we're on the right thread  If (_ pI. invokerequired = false ){ _ PI. Text = PI; _ piprogress. Maximum = totaldigits; _ piprogress. value = digitssofar; }  Else {  // Show progress Asynchronously  Showprogressdelegate showprogress =  New showprogressdelegate (showprogress );  This. Invoke (showprogress,  New object [] {Pi, totaldigits, digitssofar });  } } Void calcpi (INT digits) {stringbuilder Pi = new stringbuilder ("3", digits + 2 ); // Show progress  Showprogress (PI. tostring (), digits, 0 ); If (digits> 0) {pi. append ("."); For (INT I = 0; I <digits; I ++ = 9 ){... // Show progress  Showprogress (PI. tostring (), digits, I + digitcount ); }}}

BecauseInvokeIs a synchronous call and we're not consuming the return value (in fact,ShowprogressDoesn't have a return value), it's better to useBegininvokeHere so that the worker thread isn' t held up, as shown here:

 
Begininvoke (showprogress, new object [] {Pi, totaldigits, digitssofar });

BegininvokeIs always preferred if you don't need the return of a function call because it sends the worker thread to its work immediately and avoids the possibility of deadlock.

Where are we?

I 've used this short example to demonstrate how to perform long-running operations while still showing progress and keeping the UI responsive to user interaction. to accomplish this, I used one asynch delegate to spawn a worker thread andInvokeMethod on the main form, along with another delegate to be executed back in the UI thread.

one thing I was very careful never to do was to share access to a single point of data between the UI thread and the worker thread. instead, I passed a copy of the data needed to do the work to the worker thread (the number of digits ), and a copy of the data needed to update the UI (the digits calculated so far and the Progress ). in the final solution, I never passed references to objects that I was sharing between the two threads, such as a reference to the current stringbuilder (which wowould have saved me a string copy for every time I went back to the UI thread ). if I had passed shared references back and forth, I wowould have had to use. net synchronization primitives to make sure to that only one thread had access to any one object at a time, which wowould have been a lot of work. it was already enough work just to get the callhappening between the two threads without bringing synchronization into it.

Of course, if you 've got large datasets that you're working with you're not going to want to copy data around. however, when possible, I recommend the combination of asynchronous delegates and message passing between the worker thread and the UI thread for implementing long-running tasks in your Windows Forms applications.

Acknowledgments

I 'd like to thank Simon Robinson for his post on the developmentor. net mailing list that contains red this article, Ian Griffin iths for his initial work in this area, Chris Anderson for his message-passing ideas, and last but certainly not least, mike Woodring for the fabulous multithreading pictures that I lifted shamelessly for this article.

References
    • This article's source code
    • . Net delegates: a c # bedtime story
    • Win32 multithreaded programmingBy Mike Woodring and Aaron Cohen

Chris sellsIs an independent consultant, specializing in distributed applications in. NET and COM, as well as an example uctor for developmentor. He's written several books, includingATL Internals, Which is in the process of being updated for atl7. he's also working onEssential Windows FormsFor Addison-Wesley andMastering Visual Studio. NETFor O 'Reilly. In his free time, Chris hosts the Web Services devcon and directs the Genghis source-available project. More information about Chris, and his various projects, is available at http://www.sellsbrothers.com.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.