by Russell Jones |
March 10, 2003
Introduction
Starting a new thread is simple in VB.NET—you add the System.Threading namespace to your module to simplify naming, create a new thread using a delegate to tell it which method to start in, and then call its Start method to begin execution.
For example, suppose you have a button on a form; when the user presses the button, you want to launch a separate thread that performs some task. For this article, a simple counter method suffices to simulate a long-running procedure.
Note: The downloadable code for this article contains both VB.NET and C# versions.
Count()But rather than showing the user an hourglass cursor and disabling the button while the task executes, you want to let the user continue to interact with the program. Create a new Windows Form with a button named btnLaunchThread and set its Text property to Launch Thread. Next, create a Count() method that counts from 1 to 25, writing the current counter value to the Output window. Because that would normally happen very quickly, the code also uses the Thread.CurrentThread method to get the currently executing thread, and causes it to sleep for 100 milliseconds, which simulates a lengthy process better. Here's the code.
private void Count()
{
for (int i = 1; i < 26; i++)
{
Console.WriteLine(i.ToString());
Thread.Sleep(100);
}
}
btnLaunchThread_ClickRun the code and click the Launch Thread button. You'll see the output window slowly fill with numbers from 1 to 25. Click the button again—in fact, click it several times. What happens? Each time you click the button, the application launches a new thread, which then starts displaying numbers in the output window. To make things a little clearer, you can assign each thread a name and display that as well. The sample Form1.vb file contains the altered code:
private void btnLaunchThread_Click(object sender, System.EventArgs e)
{
Thread t = new Thread(new ThreadStart(this.Count));
t.IsBackground = true;
t.Start();
}
Show Thread Name
private void btnLaunchThread_Click(object sender, System.EventArgs e)
{
Thread t = new Thread(new ThreadStart(this.Count));
threadCount += 1;
t.Name = "Thread " + threadCount.ToString();
t.IsBackground = true;
t.Start();
}
private void Count()
{
for (int i = 1; i < 26; i++)
{
Console.WriteLine(Thread.CurrentThread.Name + ": " + i.ToString());
Thread.Sleep(100);
}
}
Here's where the simplicity breaks down a little. The Thread class constructor accepts only a ThreadStart delegate (the delegate that represents the method in which to start the thread), and there's no overloaded Thread.Start() method that accepts parameter values.
Pass Parameters to Threads with a Class
The trick is to start the thread in the context of a class that already has the parameter values you want to send. Rather than launching the new thread directly from the btnLaunchThread_Click event, you create a new class that has properties to hold the ThreadStart delegate and the number of counter iterations.
The CounterArgs class serves that purpose. The public iterations field holds the number of counter iterations, and the startDelegate field holds a StartCounterDelegate, which is a pre-created delegate that matches the Count() method in the form. The class has one method, StartCounting(), which calls the method represented by the StartCounterDelegate (the Count() method) and passes the number of iterations. In other words, to launch a new thread and pass the number of iterations, you create a new CounterArgs class, set its Iterations property, and then create a new StartCounterDelegate which represents the Count() method. Finally, you create a new Thread with a ThreadStart delegate that represents the CounterArgs.StartCounting method. When you start the new thread, it runs in the CounterArgs class, and therefore it has access to the values of the Iterations and StartDelegate fields.
Here's the complete code for the CounterArgs class.
CounterArgs
private class CounterArgs
{
public int Iterations = 0;
public StartCounterDelegate StartDelegate;
public void StartCounting()
{
StartDelegate(Iterations);
}
}
public void Count(int iterations)
{
for (int i = 0; i <= iterations; i++)
{
Console.WriteLine(Thread.CurrentThread.Name + ": " + i.ToString());
Thread.Sleep(100);
}
Console.WriteLine("Ending Thread: " +Thread.CurrentThread.Name);
}
btnLaunchThread_Click_1
private void btnLaunchThread_Click_1(object sender, System.EventArgs e)
{
int iterations = 0;
try
{
iterations = Int32.Parse(this.txtIterations.Text);
}
catch
{
MessageBox.Show("Invalid entry. Enter an integer value.");
return;
}
CounterArgs ca = new CounterArgs();
ca.Iterations = iterations;
ca.StartDelegate = new StartCounterDelegate(this.Count);
Thread t = new Thread(new ThreadStart(ca.StartCounting));
threadCount += 1;
t.Name = "Thread " + threadCount.ToString();
Console.WriteLine("Starting thread " + t.Name + " to count " + iterations.ToString() + " times.");
t.IsBackground = true;
t.Start();
}
You might ask: Why not just have the Count() method grab the iteration parameter value from the txtIterations control? That's a good question. The answer is that Windows Forms are not thread-safe—they run on a single thread, and if you try to access the controls on a form from multiple threads, you're bound to have problems. You should only access Windows Forms controls from the thread on which they were created. In this article, I've called that the "main form thread".
To make matters worse, you can access the controls from other threads without causing an exception—despite the fact that you shouldn't. For example, if you write code to grab the txtIteration.Text value, it will usually work. More commonly, you want to write to Windows Forms controls from multiple threads, for example, changing the contents of a TextBox—and because that alters the control data, it's where most control threading problems occur.
The sample Form3.vb file shows you how to access and update control values safely. The form launches the threads in the same way you've already seen, but displays the results in a multi-line TextBox. To access Windows controls safely from a different thread, you should query the InvokeRequired property implemented by the Control class and inherited by all the standard controls. The property returns True if the current thread is not the thread on which the control was created—in other words, a return value of True means you should not change the control directly from the executing thread. Instead, call the control's Invoke() method using a delegate and (optionally) a parameter array of type Object. The control uses the delegate to call the method in the context of its own thread, which solves the multithreading problems. This is the only thread-safe way to update control values.
In Form 3, the button click code is identical to the code in Form 2—it creates a thread and starts it. The Count() method is different; it contains the check for InvokeRequired, and a SetDisplayText() method to handle the control update. The Count() method code looks more complicated than it is.
Count(int)
private void Count(int iterations )
{
Thread t = Thread.CurrentThread;
for (int i = 0; i <= iterations; i++)
{
if (this.txtDisplay.InvokeRequired)
{
this.txtDisplay.Invoke(new ChangeTextControlDelegate(this.SetDisplayText),
new Object[] {t.Name, txtDisplay, i.ToString()});
}
else
{
this.SetDisplayText(t.Name, txtDisplay, i.ToString());
}
if (this.txtDisplay.InvokeRequired)
{
Thread.Sleep(100);
}
}
if (this.txtDisplay.InvokeRequired)
{
this.txtDisplay.Invoke(new ChangeTextControlDelegate(this.SetDisplayText),
new Object[] {t.Name, txtDisplay, "Ending Thread"});
}
else
{
this.SetDisplayText(t.Name, txtDisplay, "Ending Thread");
}
}
private delegate void ChangeTextControlDelegate(
string aThreadName,
TextBox aTextBox,
string newText);
SetDisplayText
public void SetDisplayText(string aThreadName, TextBox aTextBox, String newText)
{
if (aTextBox.Text.Length + newText.Length > aTextBox.MaxLength)
{
aTextBox.Text = aTextBox.Text.Substring(aTextBox.Text.Length - 1000);
}
aTextBox.AppendText(aThreadName + ": " + newText + System.Environment.NewLine);
}
Why Not Always Use Control.Invoke?
Strictly speaking, you don't need to check the InvokeRequired property for this particular application, because here only new threads call the Count() method, so InvokeRequired always returns True, and the Else portion of the two If structures never fires. In fact, if you're willing to take a miniscule performance hit, you can simply eliminate the check and always use the Invoke method. Calling Invoke from the main form thread doesn't raise an exception. You should try it yourself, to see what happens.
Here's a simplified version of Count that always uses Invoke.
Count - always Invoke
public void Count(int iterations)
{
int i;
for (i=0;i<iterations;++i)