Many articles exist on the web regarding properly handling multiple application threads and accessing data across those threads, but this article will aim to remove much of the ambiguity or inaccurate information found in many other articles.
What is Multi-Threading?
Many programmers and non-programmers alike are mystified by how multi-tasking operating systems are able to perform so many tasks simultaneously. I think it's important to note that, in truth, no processor is capable of doing "two things at once". The way "parallel execution" is achieved is by granting a tiny amount of processing time to each application, allowing each application to process a few lines of code at a time, giving the appearance that they are all running at once. In programming terms, this is achieved through the method of "multi-threading". In this context, each "thread" is a separate set of instructions to be processed in parallel. It's important to understand the concept of what really goes on inside the processor during multi-threaded or "parallel" processing.
How Does Multi-Threading Work?
For the purposes of an example, let's say we have an application that needs to analyze a photograph and try and find what appear to be people's faces. This would require a very complex set of mathematical algorithms to be run against every pixel or colored point within the image. For a large image, this could take a very long time.
Most code statements that perform some kind of data manipulation are "blocking". That is, they prevent the next line of code from executing until they have finished executing. This is called "Synchronous Execution" because each line of code is executed in sequence.
In a multi-threaded environment, child threads can be spawned by existing threads, enabling the parent thread to continue execution while the child thread performs its work. A statement that creates one of these child threads and immediately returns to execute the next line of code is called "Non-blocking". This is called "Asynchronous Execution" because multiple lines of code appear to execute simultaneously.
Let's put it in a metaphor. Imagine your computer's processor is a highway. When there's only one lane, only one car can travel down the road at a time. If a car drives slowly, then every other car behind it is forced to slow down and wait on it. Now, imagine each thread in your application is an additional lane. Now the cars can travel side-by-side, and some can even outrun others. Now think about the freeway being built out of lines of code and the cars represent the execution of that code.
There are many things to consider when your application runs multiple threads including data synchronization and stability. You wouldn't want Thread A assigning a value to a variable and then Thread B changes that value before Thread A is able to use it. This is what's known as a "Race Condition" (really, not just because I'm using a car analogy). Classes which contain code to prevent these race conditions are considered "Thread Safe". Many classes within the .NET framework are thread safe, but many are not.
The most commonly misused set of .NET types with multi-threading are the
System.Windows.Forms controls. These classes are not thread-safe, yet time and again you will find code with improper handling of these objects across thread boundaries. Going back to our highway analogy, thread boundaries would be the lines which define each lane. If one car jumps over to another lane without taking the proper steps of using a turn signal and making sure the lane is clear and... CRASH!
Types which are not thread-safe are like cars with only a front windshield and no side windshields. They can't possibly see what the other cars are doing around them, so the other cars have to make sure they follow the proper procedures if they intend to change lanes.
Now to get more specific with Windows Forms controls... All Windows Forms controls inherit from a base class called "
System.Windows.Forms.Control". Pretty obvious there. This "
Control" class exposes a
public Boolean property called "
InvokeRequired". This property lets you know if it's ok to access the control from the current thread if it returns a
Basically, this property tells you if it's necessary to invoke the required action on the thread that originally created the control, which would be the case if a child thread was trying to access a control created on the main thread or vise-versa.
"Invoking" is a term used to describe when one thread requests another thread to execute a piece of code. This is accomplished through the use of "delegates".
Delegates are a type-safe way to pass a reference to a method between objects. Here's a basic delegate definition:
delegate void ChangeMyTextDelegate(Control ctrl, string text);
This defines a reference to a method with a return type of "
void" and takes two parameters:
String. Take a look at the following method that tries to update the text of a specified control.
public static void ChangeMyText(Control ctrl, string text);
ctrl.Text = text;
This is all well and good, assuming that the thread that calls this method is the same thread that created the control. If not, we risk having our cars crash into one another.
To counter this, we use the Control's "
InvokeRequired" property and the delegate we defined a moment ago. The resulting code looks like this:
delegate void ChangeMyTextDelegate(Control ctrl, string text);
public static void ChangeMyText(Control ctrl, string text)
ChangeMyTextDelegate del = new ChangeMyTextDelegate(ChangeMyText);
ctrl.Invoke(del, ctrl, text)
ctrl.Text = text;
The first line here declares the definition for our delegate. Like before, we define it as a method that returns
void, and takes two arguments. Then comes our actual method. The first thing this method does is check to see if we're on the correct thread to access the
Control object '
ctrl' passed as the first argument. If an invoke is required, we create a new instance of our delegate and point it at the "
ChangeMyText" method. Then, we ask the control to execute the code for us by passing the delegate and the arguments the delegate requires. You might have noticed by now that the delegate's signature matches the signature of the method it represents. This must always be
true for a delegate to reference a method.
After the control executes this method for us (on its own thread), "
InvokeRequired" will evaluate to
false allowing the '
else' block to execute and set the '
Text' property of the control to the
string specified in the '
Which Statements Are "Blocking"?
As a rule, practically every statement you code will be blocking. The exception to this rule is if you call a method that begins with the word "
Begin". This is a typical coding standard which identifies non-blocking method calls. It's a good syntactic rule to follow when writing your own multi-threaded code, especially if you're writing a reusable class library, since it will help maintain constancy if you hand your DLL to someone else to use. Besides the "
Invoke" method we used above, the
Control class also exposes a "
BeginInvoke" method which does exactly the same thing, except it's a non-blocking statement.
Where Do We Go From Here?
Check out the attached file "CrossThreadUI.cs", which is a single file from a larger class library I've written. Not only does it contain a method almost identical to the example above (called "
SetText" in the .cs file), but it also exposes a number of other
static methods which enable you to set just about any property on any type of
Control object, and some even use reflection to validate the object and argument types. It's a useful helper class and a great way to experience some multi-threaded Windows Forms Control access in action.
Points of Interest
Especially be sure to check out the
InvokePropertyMethod in the source code. It will use reflection against the provided instance to find the specified property value of that instance and allow you to execute a method of that property's type, all while maintaining thread-safety. There are several methods included in the source code that allow you to work with a control's property's members.
One interesting thing to note, here, that I discovered while writing this code: All of this source can be easily ported to VB.NET by simply making the necessary syntax changes with the exception of the
ShowMessageBox method. This is because, while VB.NET does not support interfaces the same way as C# so the
System.Windows.Form object in VB.NET does not inherit from a
IWin32Window interface as it does in C#. If you are attempting to re-write this code to VB.NET, it will be necessary to change the Type of the first argument for this method to
System.Windows.Form in order to get the code to work. It will compile with the current interface designation, but will throw an exception if you attempt to pass an instance of type
System.Windows.Form to it.
This article was originally posted (by me) on CSharpCorner on April 23rd, 2009. I do realize that the comment about the face-finding being a time-consuming process is probably not true at this point.