One of the issues which frequently comes up in newsgroups is how to handle threading in a UI. There are two golden rules for Windows Forms:
Invoke
, BeginInvoke
, EndInvoke
or CreateGraphics
,
and InvokeRequired
.
Text
property) from a different
thread, you run a risk of your program hanging or misbehaving in other ways. You may get away with
it in some cases, but only by blind luck. Fortunately, the Invoke
, BeginInvoke
and EndInvoke
methods have been provided so that you can ask the UI thread to call a method
for you in a safe manner.
Application.DoEvents()
, and this is
the natural thing for many VB programmers to wish to do - but I'd advise against it. It means you have to
consider re-entrancy issues etc, which I believe are harder to diagnose and fix than "normal" threading
problems. You have to judge when to call DoEvents
, and you can't use anything which might
block (network access, for instance) without risking an unresponsive UI. I believe there are message
pumping issues in terms of COM objects as well, but I don't have details of them (and I frankly wouldn't
understand them fully anyway).
So, if you have a piece of long-running code which you need to execute, you need to create a new thread (or use a thread pool thread if you prefer) to execute it on, and make sure it doesn't directly try to update the UI with its results. The thread creation part is the same as any other threading problem, and we've addressed that before. The interesting bit is going the other way - invoking a method on the UI thread in order to update the UI.
There are two different ways of invoking a method on the UI thread, one synchronous (Invoke
)
and one asynchronous (BeginInvoke
). They work in much the same way - you specify a delegate
and (optionally) some arguments, and a message goes on the queue for the UI thread to process. If you use
Invoke
, the current thread will block until the delegate has been executed. If you use
BeginInvoke
, the call will return immediately. If you need to get the return value of a
delegate invoked asynchronously, you can use EndInvoke
with the IAsyncResult
returned by BeginInvoke
to wait until the delegate has completed and fetch the return value.
There are two options when working out how to get information between the various threads involved.
The first option is to have state in the class itself, setting it in one thread, retrieving and processing it
in the other (updating the display in the UI thread, for example). The second option is to pass the information
as parameters in the delegate. Using state somewhere is necessary if you're creating a new thread rather
than using the thread pool - but that doesn't mean you have to use state to return information to the UI.
On the other hand, creating a delegate with lots of parameters often feels clumsy, and is in some ways less
efficient than using a simple MethodInvoker
or EventHandler
delegate. These two
delegates are treated in a special (fast) manner by Invoke
and BeginInvoke
.
MethodInvoker
is just a delegate which takes no parameters and returns no value
(like ThreadStart
), and EventHandler
takes two parameters (a sender and an
EventArgs
parameter and returns no value. Note, however, that if you pass an EventHandler
delegate to Invoke
or BeginInvoke
then even if you specify parameters yourself,
they are ignored - when the method is invoked, the sender will be the control you have invoked it with,
and the EventArgs
will be EventArgs.Empty
.
Here is an example which shows several of the above concepts. Notes are provided after the code.
using System; using System.Threading; using System.Windows.Forms; using System.Drawing; public class Test : Form { delegate void StringParameterDelegate (string value); Label statusIndicator; Label counter; Button button; /// <summary> /// Lock around target and currentCount /// </summary> readonly object stateLock = new object(); int target; int currentCount; Random rng = new Random(); Test() { Size = new Size (180, 120); Text = "Test"; Label lbl = new Label(); lbl.Text = "Status:"; lbl.Size = new Size (50, 20); lbl.Location = new Point (10, 10); Controls.Add(lbl); lbl = new Label(); lbl.Text = "Count:"; lbl.Size = new Size (50, 20); lbl.Location = new Point (10, 34); Controls.Add(lbl); statusIndicator = new Label(); statusIndicator.Size = new Size (100, 20); statusIndicator.Location = new Point (70, 10); Controls.Add(statusIndicator); counter = new Label(); counter.Size = new Size (100, 20); counter.Location = new Point (70, 34); Controls.Add(counter); button = new Button(); button.Text = "Go"; button.Size = new Size (50, 20); button.Location = new Point (10, 58); Controls.Add(button); button.Click += new EventHandler (StartThread); } void StartThread (object sender, EventArgs e) { button.Enabled = false; lock (stateLock) { target = rng.Next(100); } Thread t = new Thread(new ThreadStart(ThreadJob)); t.IsBackground = true; t.Start(); } void ThreadJob() { MethodInvoker updateCounterDelegate = new MethodInvoker(UpdateCount); int localTarget; lock (stateLock) { localTarget = target; } UpdateStatus("Starting"); lock (stateLock) { currentCount = 0; } Invoke (updateCounterDelegate); // Pause before starting Thread.Sleep(500); UpdateStatus("Counting"); for (int i=0; i < localTarget; i++) { lock (stateLock) { currentCount = i; } // Synchronously show the counter Invoke (updateCounterDelegate); Thread.Sleep(100); } UpdateStatus("Finished"); Invoke (new MethodInvoker(EnableButton)); } void UpdateStatus(string value) { if (InvokeRequired) { // We're not in the UI thread, so we need to call BeginInvoke BeginInvoke(new StringParameterDelegate(UpdateStatus), new object[]{value}); return; } // Must be on the UI thread if we've got this far statusIndicator.Text = value; } void UpdateCount() { int tmpCount; lock (stateLock) { tmpCount = currentCount; } counter.Text = tmpCount.ToString(); } void EnableButton() { button.Enabled = true; } static void Main() { Application.Run (new Test()); } } |
Notes:
UpdateStatus
, which uses InvokeRequired
to
detect whether or not it needs to "change thread". If it does, it then
calls BeginInvoke
to execute the same method again from
the UI thread. This is quite a common way of making a method which
interacts with the UI thread-safe. The choice of
BeginInvoke
rather than Invoke
here was just
to demonstrate how to invoke a method asynchronously. In real code, you
would decide based on whether you needed to block to wait for the
access to the UI to complete before continuing or not. In practice, I believe
it's quite rare to actually require UI access to complete first, so I tend
to use BeginInvoke
instead of Invoke
. Another approach
might be to have a property which did the appropriate invoking when
necessary. That's easier to use from the client code, but slightly
harder work in that you would either have to have another method
anyway, or get the MethodInfo
for the property setter in
order to construct the delegate to invoke. In this case we actually know
that BeginInvoke
is required because we're running in the
worker thread anyway, but I included the code for the sake of completeness.
EndInvoke
after the BeginInvoke
. Unlike all other
asynchronous methods (see the later section on the topic) you don't
need to call EndInvoke
unless you need the return value of the delegate's method.
Of course, BeginInvoke
is also different to all of the other asynchronous methods
as it doesn't cause the delegate to be run on a thread pool thread - that would defeat the
whole point in this case!
MethodInvoker
delegate to execute
UpdateCount
. We call this using Invoke
to
make sure that it executes on the UI thread. This time there's no
attempt to detect whether or not an Invoke
is required. I
don't believe there's much harm in calling Invoke
or
BeginInvoke
when it's not required - it'll just take a
little longer than calling the method directly. (If you call
BeginInvoke
it will have a different effect than calling
the method directly as it will occur later, rather than in the current
execution flow, of course.) Again, we actually know that we need to
call Invoke
here anyway.
MethodInvoker
delegate is used to enable the button again
afterwards.
UpdateCount
- the UI thread would then try to
acquire the lock as well, and you'd end up with deadlock.
IsBackground=true;
) so that
when the UI thread exits, the whole application finishes. In other cases where you have a thread
which should keep running even after the UI thread has quit, you need to be careful not to call
Invoke
or BeginInvoke
when the UI thread is no longer running - you will
either block permanently (waiting for the message to be taken off the queue, with nothing actually
looking at messages) or receive an exception.
Back to the main C# page.