I did an article over threads in a command line project. In a command line project, there are less complications than it's GUI alternative. Here, I hope to go into enough detail to put to rest the confusion that threads can introduce to a GUI application. The big hurdle is around the limitation that the only thread to interact with a control is the thread that created the control. In the beginning, I saw this as a silly rule, one not worthy of throwing an exception. The more and more I use threads, the more obvious it becomes that this is less of a suggestion and more of a golden rule. So, the question begs, how do we create a thread to do something that we need a result back from? How do we get any information from a thread that's been started, such as progress, or state of operation? With callbacks, events, and the Invoke method! If we take some of the concepts I discussed in threaded CLR projects, like a payload struct, and use them here, than this will be an easy concept for you.
To start, I'll say that there is a long-named boolean variable we can set to false in the constructor of a form called "CheckForIllegalCrossThreadCalls." When set to false, is does just what you think, it will not check for illegal cross thread calls. To break down what that means, when true it will check to see if a thread tries to access a control it did not create, and when false, it will allow it. In simple applications this may be all you need, but any amount of complexity and it quickly becomes a bad idea. The problem with threads is their unpredictability. Should two threads happen to be editing the same control, things become unstable. From unintended outputs to crashing programs, it's not something you want to test, it's just better to do it right the first time, and every time. This variable, CheckForIllegalCrossThreadCalls, should remain true, by default it is.
For my little example here, the UI will consist of two progress bars and a button. Upon clicking the button, one thread starts per progress bar and they will update the progress bar they're associated with over time. This will leave the GUI thread to do nothing, at least until each thread calls their invokes. In addition to these controls, the form will have two delegates, two events, and a list of all the running threads. The events and delegates are paired. One pair is to update a progress bar, the other pair is for when a thread completes. There is also a struct to act as the payload for the threads, as defined here:
public struct ThreadPayload {
public int Min;
public int Max;
public int Delay;
public ProgressBar Target;
public MainForm Callback;
public Thread Process;
}
The Min and Max correspond to the progress bar's Minimum and Maximum properties. Delay is how long to wait in between updating values. The Target is of course the progress bar that the thread will update. The Callback's type is the form I created in this project, I need a copy of it so I can call it's Invoke method so the GUI thread actually executes some code rather than one of the non-GUI threads, it is the tether for the non-GUI thread to communicate to the GUI thread. The Process variable holds the non-GUI thread that the payload was passed to, this is for the OnThreadCompletion event that I'll tell you more about in a bit. This struct is the model of what the thread will be doing.
Now, to the delegates and events. There are two of each and all four are defined in the MainForm class (the one and only Form in the project). The delegates define what kind of methods will be called when the events are fired. Here's my definition of all four:
public delegate void ThreadCompletion(ThreadPayload payload);
public delegate void UpdateProgress(ProgressBar bar, int min, int val, int max);
public event ThreadCompletion OnThreadCompletion;
public event UpdateProgress OnUpdateProgress;
The ThreadCompletion delegate's signature is exactly like that in a CLR project, although what it does will be slightly different. The naming convention of delegates and their events is typically the event is the exact same name only pre-pended with "On." I have had no coworkers write up their own events, this is only what I have seen in code around the internet, in documentation, and the .Net framework itself. It seems like a good enough convention so I pass it on to you. The UpdateProgress delegate is specifically for updating a single progress bar, i.e. the progress bar it's passed, so that it matches the min, max and value that's also passed. If you were to allow a thread to set the progress bar to be indeterminate, you would change the parameter list to let you define a state. Whatever matches your needs. The MainForm adds a method to both events' method groups in the constructor, my simple set up does not need more than one method registered to each event. The events are wired up and defined as follows. Remember, the events are added in the constructor:
OnUpdateProgress += new UpdateProgress(MainForm_OnUpdateProgress);
OnThreadCompletion += new ThreadCompletion(MainForm_OnThreadCompletion);
public void MainForm_OnUpdateProgress(ProgressBar bar, int min, int val, int max)
{
bar.Minimum = min;
bar.Maximum = max;
bar.Value = val;
}
void MainForm_OnThreadCompletion(ThreadPayload payload) {
RunningThreads.Remove(payload.Process);
if (RunningThreads.Count == 0) {
StartThreads.Enabled = true;
}
}
The RunningThreads list will have each thread added to it just as the thread is started. Upon the thread ending, it'll be removed from the list using the Process property of the payload. The list will keep track of how many threads are created. We're using it to determine when the last thread has ended so we can re-enable the button that creates the threads. More about that button now. Since there are two progress bars, when clicked, it creates two threads and two ThreadPayloads:
private void StartThreads_Click(object sender, EventArgs e) {
Thread t = new Thread(new ParameterizedThreadStart(ThreadRun));
t.IsBackground = true;
ThreadPayload payload = new ThreadPayload();
payload.Callback = this;
payload.Min = 0;
payload.Max = 50;
payload.Delay = 1000;
payload.Target = progressBar1;
payload.Process = t;
RunningThreads.Add(t);
t.Start(payload);
t = new Thread(new ParameterizedThreadStart(ThreadRun));
t.IsBackground = true;
payload.Delay = 500;
payload.Target = progressBar2;
payload.Process = t;
RunningThreads.Add(t);
t.Start(payload);
StartThreads.Enabled = false;
}
Most of it is simply assigning values to the payload. Note that both threads are passed different progress bars, different delays, and then added to the RunningThreads list, also I'm using a ParameterizedThreadStart so I can communicate the ThreadPayload to the thread, since the payload contains a MainForm to callback to, that's how the communication flows the other way. Now let's get to the meat of things, what the thread actually does! It's short, but does some serious stuff!
private void ThreadRun(object Payload) {
ThreadPayload payload = (ThreadPayload)Payload;
for (int i = payload.Min; i <= payload.Max; i += 5) {
payload.Callback.Invoke(payload.Callback.OnUpdateProgress,
payload.Target, payload.Min, i, payload.Max);
Thread.Sleep(payload.Delay);
}
payload.Callback.Invoke(payload.Callback.OnThreadCompletion, payload);
}
Since a ParameterizedThreadStart passes an object, the first line unwraps the object back to it's expected type. The for statement is just to generate a new value for the paired progress bar. The first line in the for statement is an Invoke. It does all the cross-thread communication. The Invoke takes in what method to call and what parameters to pass. It's simpler to read if you don't use anonymous methods, but should you still want to, you should wrap the delegate() { } in parentheses and cast it as an Action. As an anonymous method you wouldn't need the parameter list, otherwise you could pass one object at a time in the Invoke parameter list. Next is the delay, this is simply to tell that one thread is doing one thing, while the other is doing something else, it's simply for visualization. After incrementing the progress bar to it's max, the MainForm in the payload is notified that it has completed. Because the list of RunningThreads is an array, it's also good that only one thread accesses it. Syntactically, we could have removed the thread from the list from the thread, but it'd be a possibility for two threads to attempt to remove themselves at the same time, introducing all sorts of problems. It's better this way to just have one thread that interacts with the array.
To better break down how the Invoke method works, is to remind ourselves how the Windows Messaging Queue works. Here's a short recap if you forgot. When an event such as resizing a window or clicking a button happens, it's added to a queue that the GUI thread is constantly checking. When something is in the queue, the thread will run that event and continue checking the queue. When an event in the queue takes a long time, other things in the cue are not handled; this is why Windows will claim that a window is "Not Responding" because it's not responding to stuff in the queue. Remember this, because when you use the Invoke method because that's how one thread can add an event to another thread's Messaging Queue. If you invoke a time consuming method, the program's UI and other events will lag behind.
So, in the big picture, you call Invoke on a GUI element (anything that's a subclass of Control) so that only the thread that created the control will manipulate the control. Although events are not necessarily the only methods you can invoke, it allows your thread to be loosely coupled, especially if you write your own subclass of Thread. Generally, things become stable and your application will scale better as you add more to it.
Download the sample project here.
Advice, C#, Development, Source Code
invoke, threads, windows message queue, GUI