Threads... I'm not going to claim to be an expert at explaining all the low level happenings or all the intricate little bugs and/or features of them. I am going to be talking about good foundation code for making threads work right without worrying about races, locks, or GUI halts. First, a basic understanding of what a thread is. A thread allows for multiple lines of execution to be running in one application at the same time. You may need this for many different reasons. With only one thread, that thread is in charge of what's called the message loop. It'll listen and respond to events such as repainting the form and user interactions (clicking a button, dragging the window), if you put that thread to use doing something that takes a lot of time (say you click a button and that does a lot of network traffic or image processing), then the message loop goes unattended, this is how Windows decides if an app is "Not Responding" (when the message loop accumulates events) until it completes whatever it's doing, then it begins responding to the message loop again. With another thread, you can handle the heavy load and the original thread can continue responding to events in the message loop. This can be both a good thing and a bad thing.
There are many problems that can arise in multi-threaded application. Let's say you had an array, and in one thread you're getting element 6 in that array and in another array you're inserting an element into index 2. Different things could happen based on which thread gets there first, and it's impossible to tell which will. You can always use a lock statement to prevent data corruption due to multiple threads accessing it, but that can lead to thread locks and isn't always the easiest thing to do. Instead, design your application to be thread safe from the ground up. It's easier to hammer out the details to a thread safe app first and then implement it than it is to design a semi-working app and fix the thread issues once they've become prevalent. Defining set rules for how to be thread safe is hard. The knowledge comes with experience in many different applications.
There are multiple situations in which you need to pass information to and from threads. I'm going to cover a few different "thread architectures" to cover some of these scenarios. By no means are these the only ways to use threads, but these should get you safely up and going in no time.
First, how to create and run a thread. All you'll need is in the System.Threading namespace. Once you have that included, you can begin constructing a Thread object. Let's break down some code:
static void Main(string[] args) {
Thread t = new Thread(new ThreadStart(SlowCount));
t.Start();
for (int i = 0; i < 10; i++) {
Console.WriteLine("Main: " + i);
Thread.Sleep(500);
}
}
private static void SlowCount() {
for (int i = 0; i < 10; i++) {
Console.WriteLine("Thread: " + i);
Thread.Sleep(1000);
}
}
This is simply a console application. The first line creates a new Thread object. The Thread constructor takes either a ThreadStart or a ParameterizedThreadStart (more on this in a bit), which take a method name for what method to start execution in the new thread. There's also a lesser used optional parameter to define the the stack count for that particular thread. Instead of writing a regular method, you could also have used an anonymous method like such:
Thread t = new Thread(new ThreadStart(delegate() {
for (int i = 0; i < 10; i++) {
Console.WriteLine("Thread: " + i);
Thread.Sleep(1000);
}
}));
Running this program, you'll see that the main thread and the created thread both count from 0 to 9 before both finishing and the application exits. You may notice that sometimes the second thread will execute before the main thread, and sometimes not, it is unpredictable to know exactly when the second thread will execute what lines of code over any other thread. If you run it, you'll also see that the main thread finishes before the created thread. When the main thread finishes, it lets all non-background threads continue running and will execute when no non-background threads are running. All it takes to make your thread a background thread is to set IsBackground to true on your thread object. Now this is fine and everything, just doing a static section of code, but eventually you'll need a way to provide input. That's where the ParameterizedThreadStart that I mentioned earlier comes into play. It'll allow you to pass one object to your method. This object might just be an int or a string, but often times you'll need to pass more than one thing. To accomplish this, you can pass an instance of a "Payload" struct.
public struct ThreadPayload {
public int StartingValue;
public int WaitTime;
public int Iterations;
}
public static void Main(string[] args) {
Thread t = new Thread(new ParameterizedThreadStart(SlowCount));
ThreadPayload payload = new ThreadPayload();
payload.StartingValue = 10;
payload.WaitTime = 750;
payload.Iterations = 10;
t.Start(payload);
for (int i = 0; i < 10; i++) {
Console.WriteLine("Main: " + i);
Thread.Sleep(500);
}
}
public static void SlowCount(object load) {
ThreadPayload input = (ThreadPayload)load;
for (int i = 0; i < input.Iterations; i++) {
Console.WriteLine("Thread: " + (input.StartingValue + i));
Thread.Sleep(input.WaitTime);
}
}
These small changes have allowed SlowCount to be a much more valuable and variable function, giving it adjustable functionality. There is a caution you have to take here, because you're passing an object, you could pass one kind of object and the thread body try to cast it as a different kind of object, this is a type convention that you have to enforce. This is great for passing things to a thread. Naturally the next question is how to achieve output from a thread. The best answer I've come across is a callback method. A true callback method will use the Invoke method, given that my example is a console application, I won't go that far and over complicate my example. Look for a "Threads and GUI" article to be posted eventually. Since we're going to have both input and output, let's change the name of our ThreadPayload struct to ThreadInput and we'll need another struct for the output, ThreadOutput. Also, we'll need a function for the callback to actually call back to. I usually use a OnThreadCompletion sort of layout, but it's possible to create multiple events to have your thread report in multiple places throughout its lifetime, e.g. OnFirstDataProcessed, OnSecondDataProcessed and so on. None of these events are an existing part of the .Net framework, so we'll have to implement them ourselves. More on custom delegates and events later, you'll have to trust me on this for now. After making these changes, we have input and output:
public struct ThreadInput {
public int StartingValue;
public int WaitTime;
public int Iterations;
}
public struct ThreadOutput {
public int TotalRuntime;
public bool CompletedSuccessfully;
}
public delegate void ThreadCompletion(ThreadOutput output);
public static event ThreadCompletion OnThreadCompletion;
public static void Main(string[] args) {
Thread t = new Thread(new ParameterizedThreadStart(SlowCount));
ThreadInput payload = new ThreadInput();
payload.StartingValue = 10;
payload.WaitTime = 750;
payload.Iterations = 10;
OnThreadCompletion += ThreadCompleted;
t.Start(payload);
for (int i = 0; i < 10; i++) {
Console.WriteLine("Main: " + i);
Thread.Sleep(500);
}
}
public static void SlowCount(object load) {
ThreadInput input = (ThreadInput)load;
for (int i = 0; i < input.Iterations; i++) {
Console.WriteLine("Thread: " + (input.StartingValue + i));
Thread.Sleep(input.WaitTime);
}
ThreadOutput output = new ThreadOutput();
output.CompletedSuccessfully = true;
output.TotalRuntime = input.WaitTime * input.Iterations;
if (OnThreadCompletion != null) {
OnThreadCompletion(output);
}
}
public static void ThreadCompleted(ThreadOutput output) {
String print = "";
if (output.CompletedSuccessfully) {
print = "Thread executed in approx. " + output.TotalRuntime + "ms.";
} else {
//It'll never fail in this example
print = "Thread failed in approx. " + output.TotalRuntime + "ms.";
}
Console.WriteLine(print);
}
Now we have ourselves multiple threads running in a way that you can pass input to the threads and receive output from the threads when they're finished. In a GUI application, our callback methods would use the Invoke method on any Control object and the GUI thread would run the completion code instead of the thread. The Invoke method is to ensure that the only thread making changes to the controls is the thread that created them, this prevents corruption and ensures integrity of data. This will give you a great start and lead you to try new things that you wouldn't have done before in a single threaded application.
Advice, C#, Development, Source Code
thread, delegate, event, lock, concurrency