Multithreading in WinForms(转摘)
The .NET Framework has full support for running multiple threads at once. In this article, Patrick Steele looks at how threads accomplish their task and why you need to be careful how you manage a WinForms application with multiple threads.
A user interface that is unresponsive will frustrate your users. If it goes on for too long, they bring up Task Manager and see the dreaded "(Not Responding)" next to your application. Out of frustration, they click the "End Process" button, kill your application and start from the beginning.
But this doesn't have to happen. The .NET Framework has full support for running multiple threads at once. In this article, we'll look at how threads accomplish their task and why you need to be careful how you manage a WinForms application with multiple threads.
Thread Basics
A thread is the basic unit of which an operating system uses to execute code. Every process that is started uses at least one thread. For .NET applications, the framework will spin up a couple of threads for housekeeping (garbage collection, finalization queue, etc...) and then one thread for the AppDomain. A .NET process can have multiple AppDomains and an AppDomain supports multiple threads. For this discussion, we're just concerned about the one thread that gets our application running.
Let's start with a simple console application:
using System; namespace HelloWorld { class Program { static void Main(string[] args) { Console.WriteLine("Hello, Visual Studio Magazine!"); Console.ReadKey(); } } }
When running this application, the .NET Framework creates a new AppDomain with a single thread. That thread is then instructed to start running code at the "Main" method. The first thing it does is writes our "hello" message to the console. After that, it calls the ReadKey method. This waits for the user to press any key. This is called a "blocking operation" since the thread is blocked and can't do anything until a key is pressed. The blocking happens somewhere deep inside the call to ReadKey (because that's where our thread is running code). Once a key is pressed, the thread is done and the application exits.
Some Multithreading
Let's add a little bit of multithreading to our console application:
1 using System.Threading; 2 3 namespace HelloWorldMultiThreaded 4 { 5 internal class Program 6 { 7 private static void Main(string[] args) 8 { 9 Console.WriteLine("Start counting..."); 10 StartCounting(); 11 Console.WriteLine("Press any key to exit..."); 12 Console.ReadKey(); 13 Console.WriteLine("Exiting..."); 14 } 15 16 private static void StartCounting() 17 { 18 var thread = new Thread(() => 19 { 20 for (var x = 0; x < 10; x++) 21 { 22 Console.Write("{0}... ", x); 23 Thread.Sleep(1000); 24 } 25 }); 26 thread.Start(); 27 } 28 } 29 }
Let's take a step by step approach to see what is going on:
- Our first (and currently only) thread starts at the Main() method.
- Console.WriteLine is called.
- The first thread jumps to the StartCounting method.
- A new thread object is created and points to a lambda (anonymous delegate) as the code it should execute.
- The first thread then starts up the second thread (The second thread is simply a loop that prints the numbers 0 through 9 with a one second pause between each number).
- StartCounting is done and the first thread returns to the Main method.
- The first thread calls ReadKey() and is blocked.
- The second thread was started in step 5 continues to output the numbers 0 through 9 as our first thread is blocked.
- Our second thread blocks itself from time to time by executing the Thread.Sleep(1000), but this affects only the second thread.
- When the user presses a key, the first thread continues on to the WriteLine and then it is done.
- The second thread will continue to output the numbers 0 through 9. When the loop completes, the second thread is done.
- Once both threads are done, the application exits.
Run this code and play around with it. Note that if you press a key before the second thread is done counting to 9, you'll see the "Exiting..." message, but the numbers continue to print even though the Main method's thread has exited. That's because the second thread is a foreground thread.
All threads behave the same way and all threads are defined as either a foreground thread (the default) or a background thread. The only difference is how the .NET runtime treats them. A .NET application won't exit until all foreground threads have completed. In our example, the second thread is a foreground thread and therefore, the counting will continue even though the Main thread for the application has ended.
If you want to see the difference a background thread makes, we can mark the second thread as a background thread by calling:
thread.IsBackground = true;
Right before we start the thread at "thread.Start()". Now if you run the application, you'll notice that a key press during the second thread's counting will end the application. Since the first thread is the only foreground thread, the .NET Framework will end the application (and stop all background threads) as soon as the Main method exits.
WinForms and the Message Pump
Windows applications (either WinForms in .NET, or C/C++ applications) are driven by messages being sent to them from the operating system. The OS will tell you when:
- The mouse is moved.
- A key is pressed.
- A window is moved.
- The left mouse button is pushed down.
- A paint message
- Hundreds of other "messages"
How is all of this messaging coordinated in your application? The messages are placed in a FIFO (first in, first out) queue and your application pulls them out one by one and processes them. At the heart of almost every windows application is code that looks basically like this (simplified pseudo-code):
while(msg = get_next_message())
{
dispatch_message(msg);
}
This is called the message pump. What this code does is monitor the Windows message queue. When a message appears, it pulls the message and dispatches it to your application. Dispatching means it looks to see if you have an event handler set up for the particular message. When the user clicks on a button, the .NET Framework will determine which button was clicked and if you have an event handler subscribed for the "Click" event of the button. If so, the framework then calls your event handler and your code is executed.
This message pump handles messages for your entire application. Every control on all of your forms can have a message sent to it (a message to paint itself, a message to scroll, etc...). All of these messages get dispatched via this message pump.
If you look at a basic WinForms application in C#, you'll notice the Main() method found in Program.cs has the following code:
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
The very last line of code is a call to Application.Run(). This is a .NET Framework method and it encapsulates the Windows message pump I just described. The form passed to the method is considered the "main" form for your application. Once that form is closed, the message pump exits, the thread comes back to Main where there is nothing left to do and the program exits.
A Non-Responsive UI
Now that we know what is going on, let's revisit the situation that I described at the beginning of this article:
A user interface that is unresponsive will frustrate your users. If it goes on for too long, they bring up Task Manager and see the dreaded "(Not Responding)" next to your application. Out of frustration, they click the "End Process" button, kill your application and start from the beginning.
Why does your UI become unresponsive? Why does task manager display a "(Not Responding)" message next to your application? Let's look at what happens when an event handler contains something like this:
private void button1_Click(object sender, EventArgs e)
{
Thread.Sleep(20*1000);
}
Obviously this is a contrived example of some code that takes 20 seconds to complete -- it's only a simulation.
If we consider that the .NET Framework started a single thread, executed the code in Main and is currently sitting inside the message pump processing messages from the operating system:
- The user moves the mouse and clicks on "button1".
- The message loop sees a message in the queue and pulls it out. In reality, there were hundreds of messages pouring into the queue as the user moved the mouse, but we don't have any event handlers set up so they were pulled out of the queue and discarded.
- The message is examined and determined to be a click on "button1".
- The .NET Framework sees that you have an event handler subscribed to button1's click event.
- The thread now jumps into your code for the click event handler.
- The thread is now doing something that keeps it busy. In our example, it's sleeping for 20 seconds. In reality, it could be an extensive database operation, a Web service call, whatever.
- During those 20 seconds, the user starts up Outlook to check their email. Your form is covered by Outlook.
- The user minimizes Outlook to return to your application. Since your form was previously covered by Outlook but is now visible, it needs to be redrawn so the operating system puts a redraw message into your queue.
- 20 seconds isn't up yet so the thread is still executing your event handler. It's not processing the message loop so the redraw that just got sent isn't being processed. And the user is shaking the mouse in an attempt to "wake up" your application (c'mon -- we all do this sometimes). This generates a bunch of mouse move messages that get pushed into the queue.
- The 20 seconds still hasn't elapsed (i.e. your event handler is still working), but the user is getting frustrated. They go into Task Manager to see what is happening.
- Windows notices that your message queue is getting quite full and that your application hasn't pulled anything out of the queue recently. It assumes your application has hung and adds that nasty "Not Responding" next to your application.
At this point, two things will happen. The user will be patient and eventually, your event handler finishes. The thread returns back to the message loop and all of those messages that have been backing up are processed. The app becomes responsive again and the user is happy, but annoyed. The other possibility is that the user, while in Task Manager, kills your app and starts over thinking your app just hung. This could cause a loss of work and loss of confidence in your application's stability.
Multithreading in WinForms
So let's do a similar multithreaded counter like we did in our console application. Instead of outputting the digits 0-9 to the console, we'll put them in a textbox. We'll start the counting when the user clicks on a button. Here's the "click" event which will start up a second thread and then allow the first thread to return to the message loop:
private void button1_Click(object sender, System.EventArgs e)
{
var thread = new Thread(StartCounting);
thread.IsBackground = true;
thread.Start();
}
private void StartCounting()
{
for (var i = 0; i < 10; i++)
{
textBox1.Text = i.ToString();
Thread.Sleep(1000);
}
}
Notice that we made the thread a background thread. We don't want this thread to prevent the application from ending so we've changed it to a background thread.
Run the code and click on the button. What? You got an exception?
System.InvalidOperationException: Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on.
What is happening? The .NET Framework is helping you identify your mistake. The textbox was created on the "main" thread (the one that is running the message pump). At this point in the code, when we're trying to update the Textbox's Text property, we're running on a different thread. Updating WinForms controls from background threads is not allowed as it can cause many issues -- not the least of which is that the thread we're running on does not have a message pump running! Without the message pump, the update of the property (which happens via a Windows message), would never be processed in this thread. If you've written code like this before in .NET Framework 1.x, you didn't get this exception. It's a new exception introduced in 2.0 to help you identify this scenario of cross-thread operations.
So now we need to somehow get the textbox update to be performed on the main thread. WinForms helps us out by providing an "Invoke" method on all Control-derived classes which will take a delegate and will marshal that delegate to the main thread (where our message pump is).
Here's an update to our code for updating the textbox:
private delegate void DisplayCountDelegate(int i);
private void StartCounting()
{
for (var i = 0; i < 10; i++)
{
textBox1.Invoke(new DisplayCountDelegate(DisplayCount), i);
Thread.Sleep(1000);
}
}
private void DisplayCount(int i)
{
textBox1.Text = i.ToString();
}
First, we define a separate delegate for displaying our count. The next change is inside the for loop. Instead of directly updating the Text property, we using TextBox1's Invoke method and give it a delegate. That delegate will be marshaled to the main thread and executed there via the message pump. Run this code and you'll see that we can move the form, resize it and do other things and the form stays responsive while the background thread runs. You'll also notice that if you close the form before the second thread has counted to 9, the application will exit.
Multithreaded Complexity
Another thing you might have noticed is that if you click the button multiple times, multiple threads are started and you'll get a mingling of updates in the textbox as each thread is running the for loop. We can prevent this by disabling the start button as soon as the second thread is started:
private void button1_Click(object sender, System.EventArgs e)
{
var thread = new Thread(StartCounting);
thread.IsBackground = true;
thread.Start();
button1.Enabled = false;
}
This works. It prevents the user for spinning up a whole bunch of threads. But it introduces another problem -- how do we re-enable the button once the thread is complete? Your initial reaction might be to enable the button after the for loop as completed:
private void StartCounting()
{
for (var i = 0; i < 10; i++)
{
textBox1.Invoke(new DisplayCountDelegate(DisplayCount), i);
Thread.Sleep(1000);
}
button1.Enabled = true;
}
But remember, we can't manipulate controls from a background thread. We'd have to create another delegate and marshal it to the main thread using Invoke:
private delegate void EnableButtonDelegate();
private void StartCounting()
{
for (var i = 0; i < 10; i++)
{
textBox1.Invoke(new DisplayCountDelegate(DisplayCount), i);
Thread.Sleep(1000);
}
button1.Invoke(new EnableButtonDelegate(EnableButton));
}
private void EnableButton()
{
button1.Enabled = true;
}
This works, but our code is starting to get a little messy. We've got delegates defined for marshalling calls back to the main thread. Those calls are executed via Invoke and any parameters are sent via an object[] (no strong typing). Because of this complexity, the .NET Framework introduced the BackgroundWorker.
The BackgroundWorker Component
The BackgroundWorker component is designed specifically for running code on a separate thread and reporting progress back to the main thread. The BackgroundWorker component supports two events for reporting progress. The big benefit of the BackgroundWorker is that these events are raised on the same thread that created the BackgroundWorker. Therefore, as long as you take care to create your BackgroundWorker on the main thread, the events will run on the main thread and you won't have to deal with delegate's and Invokes.
ProgressChanged event:This event is raised whenever the ReportProgress method is called. The ReportProgress event has two overloads. The first is a single integer argument. This usually represents the percentage of work completed (0 to 100). The second overload adds a "userState" argument of type object. This allows you some more flexibility in the type of data used to report progress in your background thread.
RunWorkerCompleted event:This event is raised once the BackgroundWorker has completed its processing.
Counting with the BackgroundWorker
Now let's look at a revised example using the BackgroundWorker to do the processing of the for loop.
public partial class Form1 : Form
{
private readonly BackgroundWorker worker;
public Form1()
{
InitializeComponent();
worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += StartCounting;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
}
private void button1_Click(object sender, System.EventArgs e)
{
worker.RunWorkerAsync();
button1.Enabled = false;
}
private void StartCounting(object sender, DoWorkEventArgs e)
{
BackgroundWorker bgWorker = (BackgroundWorker) sender;
for (var i = 0; i < 10; i++)
{
bgWorker.ReportProgress(i);
Thread.Sleep(1000);
}
}
private void worker_ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
textBox1.Text = e.ProgressPercentage.ToString();
}
void worker_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
button1.Enabled = true;
}
}
This is much simpler. In the form's constructor, we set up the BackgroundWorker. You must indicate that it will be reporting progress events by setting the WorkerReportsProgress property to true. Then we hook up the events that run the for loop, report the progress and finally, re-enable the button once the worker is completed. The StartCount method was updated to call the BackgroundWorter's ReportProgress method. That method will raise the ProgressChanged event on the main thread (no delegates or Invoke required!).
As you can see, the BackgroundWorker is the preferred way to perform long-running tasks in the background in a WinForms application. By using events that are fired on the main thread, it greatly simplifies the communications between your background task and the UI.
Conclusion
I hope this exploration into WinForms has helped you understand the complexities of multithreading. This is by no means an exhaustive guide on multithreading. My goal was to help you understand some of the lower level mechanics of windows and utilize that knowledge to make your UI's responsive as long-running operations take place.