Introduction
This is a simple example taken from MSDN about multi-threading or, the way I like to say it, "Doing two things at once." I've modified the original example to make it easier to see how everything interacts. Easier for me anyway, since we are all different. One explanation may be adequate for some and not for others. I hope this can shine some light for some, as it did for me.
Far from being any type of Guru on the subject, I'll try and explain it the best I can from my point of view. I honestly don't understand everything about threads, delegates and so on. The important thing in my mind is that I understood enough to have it do what I wanted in the end. The next step is understanding the rest.
What the Code Does
On start-up, a thread is fired that updates a label counting from 0 to 999. Buttons on the form give you the ability to start another thread that does the same thing, only at a slower pace. This shows you that both threads are separate from one another.
Background
Well, I guess it all started when I wanted to do two things at once, say, look up files in a directory. I wanted to have a nice progress bar move up and down or at least be able to move the Window around while it's doing it. If there's something I hate, it's not knowing if the application in front of me is alive or dead. After hours and hours of getting the error message, "Cross-thread operation not valid," I figured my way around it.
Updated
- 20-07-2007
Ok, so I tried to make good on my word to follow the advice and comments from people nice enough to point out bad habits. One example is Thread.Abort()
. I tried several things and attempted to understand them as best I could. What I came up with was something that does exactly the same thing, but without Thread.Abort()
. I hope this is a good thing. I haven't gotten any errors and believe me, I tried to click everywhere and at different moments to get one. An error I did get was, "Could not access an object that was in the process of disposing itself." In my case, this is the label I'm constantly trying to modify. I hope the if
statement helps. If not, I'm sure someone will point it out. Anyway, I'd like to consider this a work in progress. By getting constructive comments, I'll be in a better position to make changes, learn and hopefully help others. This is, after all, a helpful community. I left the original code fragments and pasted the new code right under it so that people can compare the two. I hope I did better this time than the last. - 25-07-2007
Another update. A few people mentioned that it would be better to use BeginInvoke()
rather than Invoke()
. Using Invoke()
makes a synchronous call to the thread, causing the thread itself to wait until the GUI is updated before continuing on its way. BeginInvoke()
, on the other hand, makes that call asynchronously. This means that the thread will continue working without waiting for the GUI to update itself. In this case, that is the label's Text
property. So if for some reason the GUI hangs, my counter will keep on working without waiting for the label to update its text property from, say, 2 to 3 and so on. For a more concrete explanation, I suggest -- as it was to me -- reading the article here by Mr. S. Senthil Kumar. I also suggest reading the comments at the end of this page from the other users. This way you'll better understand the changes and why I made them. - 07-08-2007
Another update. Got some good suggestions and a typo to fix up. First of all, thanks to everyone that has contributed. I'm disappointed in myself for not keeping the original article and seeing its evolution. However, the original code from all versions are part of the download. So, if you want to see the changes in the code itself, extract them all and check out the differences. It has be brought to my attention that I should check if a thread is stopped gracefully and, if not, force Abort()
on it. This gives a certain amount of time to the thread to join. If for some reason it doesn't do so, it will kill it with Abort()
. The code in has been changed accordingly and, as always, you should read up on the comments made by users, since the explanation is quite clear on the reasons to do so. A typo has been corrected thanks to Don Driskell; I was assigning null
twice on a thread. I guess that's it then! I don't see much more that can be done without re-writing the whole article. Of course, I'm still trying learn and understand threads. Over time, I'll try and find the time to write another article. Threads are not as simple as portrayed here, but it's a start.
Code Fragments
Ok, let's start with the references at the top of the file. When you start a new project, be sure to add System.Threading
with the others. If not, the only thread that will run will be the main application.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
Secondly, you'll need to declare a delegate and some threads. Think of them like this: The threads are little bits of code that will run parallel to each other, much like sprinters in the 100 meter dash. So, Thread threadLeft
is runner 1 and Thread threadRight
is runner 2. As for the delegate, well I try and look at it this way: It is a function that can be attached and executed in a thread other than the main thread. A better way to think of it may be that they are like pointers in C and C++.
delegate void SetTextCallback(string text);
private Thread threadLeft = null;
private Thread threadRight = null;
private volatile bool _stopLeft = false; #added this
Ok, now let's have a thread run on start-up. It's actually surprising how much code you need to do this. In my example, I have a label that will count from 0 to 999, all the while still being able interact with the form. For example, click a button and move it around. These are the parts you need. I'll have threadleft
start first.
public Form1()
{
InitializeComponent();this.threadLeft =
new Thread(new ThreadStart(this.ThreadProcSafe));
this.threadLeft.Start();
btnStartLeft.Text = "Stop Left";
}
So, what we have here is pretty simple. InitializeComponent();
prepares all of your labels, buttons, etc. When that's done, it moves on to the new thread. The simplest way to explain it is this way, which is the way in which I understand it: Create a new instance of threadleft
. When it's started, fire off the ThreadProcSafe()
function. Let's look at that function now.
The code below has been changed from this:
private void ThreadProcSafe()
{
for (int x = 0; x < 1000; x++)
{
this.SetText(x.ToString());
Thread.Sleep(600);
}
}
To this:
private void ThreadProcSafe()
{
int x = 0;
while(!_stopLeft)
{
this.SetText((x++).ToString());
Thread.Sleep(600);
}
}
Once _stopLeft
is true
, the loop is exited and stops calling the function that changes lbl_left
's Text
property. ThreadProcSafe()
is a very simple while
loop. Nothing fancy, it just increments an integer by 1 and finishes once _stopLeft
equals true
. Thread.Sleep(600);
in the loop is used to slow the counting process down. Since ThreadProcSafe()
is in a separate thread, sleep only affects the while
loop. It's also calling another function, SetText(string text)
. This is where the delegate comes in.
The code below has been changed from this:
private void SetText(string text)
{
if (this.lbl_left.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.lbl_left.Text = text;
}
}
To this:
private void SetText(string text)
{
try
{
if (!lbl_right.Disposing)
{
if (this.lbl_left.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.BeginInvoke(d, new object[] { text });
}
else
{
this.lbl_left.Text = text;
}
}
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
}
If I paraphrase the online MSDN, they say this about the Invoke
method: It provides access to properties and methods that have been exposed by an object. Well, the object exposed in our case will be the label called lbl_left
. Then we'll have access to its properties, such as Text
. So, threadLeft
starts and calls ThreadProcSafe
, in which the while
loop calls SetText
. Once we have the ability to change the label's properties, we change Text
to our integer from our loop. This will go on until you stop the application or kill the thread somehow. Since it's in its own thread, we can make a button to stop it. Here's the bit of code to do so.
The code below has been changed from this:
if (this.threadLeft != null && this.threadLeft.IsAlive)
{
this.threadLeft.Abort();
this.threadLeft = null;
SetText("Left");
btnStartLeft.Text = "Start Left";
}
To this:
if (!_stopLeft)
{
_stopLeft = true;
if (!this.threadLeft.Join(1000))
{
this.threadLeft.Abort();
}
SetText("Left");
btnStartLeft.Text = "Start Left";
}
Slap this in a button, in this case, the left button. It's actually pretty easy to understand. The reason for the if
statement is that I have the start and stop functions associated with the same button. If the thread is not running, only having this.threadLeft.IsAlive
as a condition will throw the message, "Object reference not set to an instance of an object." This basically means that it doesn't exist or is not instantiated with new
. The way around it is to check if it's null
. I know this.threadLeft = null;
is probably overkill on my part, but I just want to make sure. Have a look at the code for the right button and you'll see that it's pretty much the same thing.
Points of Interest
If you still have threads running when you click the "X" on your form, they may not stop correctly. Override OnClose
and slap it some code to check if any threads are running. This way, when you close the application with the "X," you are sure to stop the threads before closing the application.
The code below has been changed from this:
protected override void OnClosed(EventArgs e)
{
if (this.threadLeft != null && this.threadLeft.IsAlive)
{
this.threadLeft.Abort();
this.threadLeft = null;
}
base.OnClosed(e);
}
To this:
protected override void OnClosed(EventArgs e)
}
_stopLeft = true;
_stopRight = true;
if (!this.threadRight.Join(1000))
{
this.threadRight.Abort();
}
if (!this.threadLeft.Join(1000))
{
this.threadLeft.Abort();
}
base.OnClosed(e);
}
The thread is given 1000 milliseconds to execute one loop, hit the flag that says to stop and exit gracefully. If it takes more than 1000ms, then Abort()
is called on the thread to kill it forcefully. Thanks, J4amieC, you explained it well.
History
- It all started with the Big Bang!
- Then on July 18th, 2007 I posted the original version of this article.
- After reading up and taking in all the comments, on July 27th, 2007 I tried to better myself and make this thing work.
- On August 7th, I gave the threads time to join, if not abort.