荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: michaelx (好好学习), 信区: DotNET
标  题: Safe, Simple Multithreading in Windows Forms
发信站: 荔园晨风BBS站 (Sun Dec  1 12:04:32 2002), 站内信件

Safe, Simple Multithreading in Windows Forms
Chris Sells

June 28, 2002

Download the AsynchCalcPi.exe sample file from the MSDN Code Center.

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, many 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 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 progress
  ShowProgress(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 progress
      ShowProgress(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 would 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 synchronous Click handler looked like this:

void _calcButton_Click(object sender, EventArgs e) {
  CalcPi((int)_digits.Value);
}

Recall that the issue is until CalcPi returns, the thread can't return
from our Click handler, which means the form can't handle the Paint
event (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 waiting for CalcPi to finish before returning from the
button Click event, I'm creating a new thread and asking it to start.
The Thread.Start method 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 calling CalcPi in 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 hassle 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 call CalcPi
looks like this:

delegate void CalcPiDelegate(int digits);

Once I have a delegate, I can create an instance to call the CalcPi
function 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 call CalcPi synchronously; 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 from MultiCastDelegate with three
functions, Invoke, BeginInvoke, and EndInvoke, as shown here:

class CalcPiDelegate : 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 of the CalcPiDelegate earlier and then called
 it like a function, I was actually calling the synchronous Invoke
function, which in turn called my own CalcPi function. BeginInvoke and
EndInvoke, however, are the pair of functions that allow you to invoke
and harvest the results of a function call asynchronously. So, to have
the CalcPi function called on another thread, I need to call BeginInvoke
 like 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 arguments of
BeginInvoke. These are needed if we'd like to harvest the result from
the function we're calling at some later date (which is also what
EndInvoke is for). Since the CalcPi function 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 should 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 windowing
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 would 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 would 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, the .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 calls, you should use one of the invoke methods to
marshal 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 should 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 overload of Invoke takes 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 our ShowProgress method so that we
 can pass the arguments correctly. Here's how to use Invoke to make sure
 that our calls to ShowProgress, and therefore our use of our windows,
shows up on the correct thread (making sure to replace both calls to
ShowProgress in CalcPi):

delegate
void 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 < digits; i += 9 ) {
      ...
      // Show progress
      this.Invoke(showProgress,
        new object[] { pi.ToString(), digits, i + digitCount});
    }
  }
}

The use of Invoke has 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 call to Invoke is a bit cumbersome, and because it happens twice
in our CalcPi function, we could simplify things and update ShowProgress
 itself to do the asynchronous call. If ShowProgress is called from
the correct thread, it will update the controls, but if it's called from
 the incorrect thread, it uses Invoke to call itself back on the correct
 thread. This lets us go back to the previous, simpler CalcPi:

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);
    }
  }
}

Because Invoke is a synchronous call and we're not consuming the
return value (in fact, ShowProgress doesn't have a return value), it's
better to use BeginInvoke here so that the worker thread isn't held up,
 as shown here:

BeginInvoke(showProgress, new object[] { pi, totalDigits,
digitsSoFar});

BeginInvoke is 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 and the Invoke method 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
would 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 would
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 would have
 been a lot of work. It was already enough work just to get the calls
happening 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 inspired this article, Ian Griffiths for his
initial work in this area, Chris Andersen 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 Programming by Mike Woodring and Aaron Cohen

------------------------------------------------------------------------
--------

Chris Sells is an independent consultant, specializing in distributed
applications in .NET and COM, as well as an instructor for
DevelopMentor. He's written several books, including ATL Internals,
which is in the process of being updated for ATL7. He's also working
on Essential Windows Forms for Addison-Wesley and Mastering Visual
Studio .NET for 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.

--

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 61.144.235.39]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店