Title: Let a thread call a form's methods in C#
When the user clicks the Start Thread button, this program runs a counter on a separate thread. Every second the counter updates the form's Value variable. It then displays the thread's number and the new Value in the ListBox at the bottom of the form.
The following code shows how the program defines the Value variable and creates a thread.
// This value is incremented by the thread.
public int Value = 0;
// Make and start a new counter object.
private int thread_num = 0;
private void btnStartThread_Click(object sender, EventArgs e)
{
// Make a new counter object.
Counter new_counter = new Counter(this, thread_num);
thread_num++;
// Make a thread to run the object's Run method.
Thread counter_thread = new Thread(new_counter.Run);
// Make this a background thread so it automatically
// aborts when the main program stops.
counter_thread.IsBackground = true;
// Start the thread.
counter_thread.Start();
}
This code starts by defining the Value variable. It's public so the Counter class (described shortly) can see it.
The code defines a thread_num variable so it can give each thread a different number.
The Start Thread button's Click event handler creates a new Counter object, passing the constructor a form reference and the thread_num value. It then increments thread_num.
Next the code makes a new Thread object, associating it with the new Counter object's Run method. When the Thread starts, it executes that method.
The code then sets the Thread object's IsBackground property to true to make the Thread automatically stop when the main program ends.
Finally the code starts the thread.
One of the most confusing features of threads is that only the thread that created the form and its controls can access the form and its controls safely. That thread is called the UI thread. Other threads can access the form's variables, such as the Value variable defined in this example, but they cannot manipulate the form's controls. In particular they cannot display text, manipulate images, adjust scroll bars, and so forth.
To make changes to the user interface, a thread must "invoke" a method provided by the form. You'll see how in a moment when I discuss the Counter class.
The following code shows the form's DisplayValue method that other threads can invoke to indirectly display values on the form.
// Add the text to the results.
// The form provides this service because the
// thread cannot access the form's controls directly.
public void DisplayValue(string txt)
{
lstResults.Items.Add(txt);
lstResults.SelectedIndex = lstResults.Items.Count - 1;
}
This method is quite simple. It just adds a value to the form's ListBox.
That's all the form does. The rest of the fun takes place in the Counter class that is executed by the threads.
The following code shows the Counter class's constructor.
// The form that owns the Value variable.
private Form1 MyForm;
// This counter's number.
private int Number;
// Define a delegate type for the form's DisplayValue method.
private delegate void DisplayValueDelegateType(string txt);
// Declare a delegate variable to point to
// the form's DisplayValue method.
private DisplayValueDelegateType DisplayValueDelegate;
public Counter(Form1 form1, int number)
{
MyForm = form1;
Number = number;
// Initialize the delegate variable to point
// to the form's DisplayValue method.
DisplayValueDelegate = MyForm.DisplayValue;
}
This code defines variables MyForm and Number to store a reference to the form that launched the thread and the thread's ID number. Those values are passed into the constructor when the form creates the object.
Next the code defines a delegate type representing a void method that takes a string as a parameter. It creates a variable of that delegate type named DisplayValueDelegate.
The constructor simply saves the values passed to it. It then saves a reference to the form's DisplayValue method in the delegate variable for later use.
The following code shows the Counter class's Run method, which does all of the work. It looks long but it's mostly comments and error handling.
// Count off seconds in the Output window.
public void Run()
{
try
{
while (true)
{
// Wait 1 second.
Thread.Sleep(1000);
// Lock the form object. This doesn't do anything
// to the form, it just means no other thread can
// lock the form object until we release the lock.
// That means a thread can update MyForm.Value
// and then display its value without interference.
lock (MyForm)
{
// Increment the form's Value.
MyForm.Value++;
// Display the value on the form.
// The call to InvokeRequired returns true
// if this code is not running on the same
// thread as the object MyForm. In this
// example, we know that is true so the call
// isn't necessary, but in other cases it
// might not be so clear.
if (MyForm.InvokeRequired)
{
// Make an array containing the parameters
// to pass to the method.
string[] args = new string[] { Number + ": " +
MyForm.Value };
// Invoke the delegate.
MyForm.Invoke(DisplayValueDelegate, args);
}
}
}
}
catch (Exception ex)
{
// An unexpected error.
Console.WriteLine("Unexpected error in thread " +
Number + "\r\n" + ex.Message);
}
}
The code enters a loop where it sleeps for 1 second. It then uses a lock statement to lock the form. That doesn't do anything to the form; it just makes an entry somewhere that means, "No one else can lock this object (the form) until I unlock it." The locked form is used to prevent two threads from doing something at the same time in a way that would interfere with each other. In this example, the lock prevents two threads from trying to read and update the form's Value variable at the same time.
For example, suppose Value is initially 100. Now one thread increments Value to 101 and then another thread takes control of the CPU. The second thread increments Value to 102. Next the second thread displays Value (I'll explain that in a moment) and goes to sleep. At that point the first thread resumes and displays Value. At this point Value is 102 so this thread also displays the value 102. In this example, both threads display the value 102 and neither displays the value 101.
This situation where the exact order of operation by multiple threads changes the outcome is called a "race condition." Basically each thread is racing to execute its own code and the result depends on the winner. Race conditions can be extremely hard to debug because the outcome depends on the exact order in which the threads execute and that can change every time you run the code. You can run the code a million times successfully before the threads happen to execute in exactly the right sequence to cause a problem.
Locking the form means that a thread will not be interrupted by another thread while it is incrementing and displaying the value.
It doesn't matter what object you use to make the lock as long as all of the threads use the same object. The form just happens to be handy because every thread has a reference to it.
After obtaining the lock, the code increments the form's Value variable. It then calls the form's predefined InvokeRequired method to see if it must use invoke to call the form's methods. InvokeRequired returns true if the executing code (in the Counter class) is not in the same thread as the form. (The UI thread.) In this example we know that this is the case (the Counter is definitely running in a non-UI thread) so we don't really need to make this check because we know it will always be true. This example only does it to show how you can do this in more complicated situations.
If InvokeRequires is true (and it always is in this example), the code makes an array of arguments to pass to the form's DisplayValue method. It then uses the form's Invoke method to call the DisplayValue method stored in the delegate variable, passing it the array holding the string parameter. The Invoke method then executes DisplayValue on the UI thread.
Whew! An exhausting way to simply add a string to a ListBox.
To summarize:
- The thread cannot directly add items to the ListBox because it is not the UI thread.
- The thread calls InvokeRequired to see if it must use Invoke to manipulate the form's controls.
- The thread uses Invoke to call the form's DisplayValue method.
- The thread uses a lock to make sure it's the only thread that has access to the form's Value variable while it increments and displays it.
Download the example to experiment with it and to see additional details.
|