Jul/090
Multi-threading in C#
Threading is fun, because with it you can do a lot more stuff at the same time. For example, keeping your UI updated while your background tasks are running.
C# supports parallel execution of code through multi-threading. A thread is an independent execution path, able to run simultaneously with other threads. Here, we examine three simple C# approaches to help your application perform multiple tasks simultaneously.
- Method 1: Using System.Thread (easy but unsafe, for low risk use)
- Method 2: Using Application.DoEvents (okay for general purposes)
- Method 3: Using BackgroundWorker class (preferred for thread safety)
Of the three methods listed above, the method 1 is the easiest, but is considered unsafe. However, I still find it useful as a quick-n-dirty implementation for rapid prototyping developing, running simulations or testing out new algorithms. It also provides for simple pause and resume mechanisms.
Download: Demo using System.Threading.
Method 2 is an event-driven approach that applies asynchronous event handlers, and then calls Application.DoEvents to repaint the UI. Hence, its more like multi-tasking rather than multi-threading. In addition, it has a slight re-entry flaw supposed inherited from its predecessors.
Download: Demo using Application.DoEvents.
For now, it seems like method 3 is the generally accepted preferred practice using thread-safe methods. Its a bit more tedious to implement neatly. I have included source code to provide a short framework to illustrate its capabilities.
Download: Demo using BackgroundWorker.
Method 1: Using System.Threading (easy)
Using System.Threading for threading purposes is bad. The class exposes two dangerous methods Suspend() and Resume() that are considered unsafe, and are given deprecated warnings since .NET 2.0. However, I still find it useful as a quick-n-dirty implementation for rapid prototyping developing, running simulations or testing out new algorithms. It also provides for simple pause and resume mechanisms. Simplest approach if you need to show a prototype to your client quickly.
Nevertheless, its important to understand that its major problem is that of deadlocks. You have no way of knowing what code a thread is executing when you suspend it. If you suspend a thread while it holds locks during a security permission evaluation, other threads in the AppDomain might be blocked. If you suspend a thread while it is executing a class constructor, other threads in the AppDomain that attempt to use that class are blocked. Deadlocks can occur very easily. Here’s an example of a program exhibiting deadlock: http://www.yoda.arachsys.com/csharp/threads/deadlocks.shtml
So at any point in time, the thread could be doing anything. Executing suspend stops the thread running in its tracks. Suspended. Hence, imagine your thread is reading a file and places a lock on it. You suspend your thread. File stays locked. Same goes for any other resources.
Anyway, if you need a quick solution, here is an outline of how the demo source above works. The UI object starts a new thread for the task object. Hence the UI object and the task object run on separate threads. The task object then performs the endless loop. On each iteration, the task object has to perform an invoke to update the UI object on a cross-thread. Thats it, easy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class MainForm { // ... void StartThread(object sender, EventArgs e) { // creates the new thread and starts it _thread = new Thread(new ThreadStart(DoThreadTask)); _thread.Start(); } // creates the task object and runs it void DoThreadTask() { WorkTask task = new WorkTask(); task.StartEndlessLoop(this); } // invoked by the task object to update the UI internal void RefreshForm(int i) { txtSomeText.Text = i.ToString(); } } class WorkTask { internal void StartEndlessLoop(MainForm caller) { // ... caller.Invoke((MethodInvoker)delegate() { // when we get here we are now in the context // of the primary user interface thread, hence // we can modify all of it's controls caller.RefreshForm(i); }); } } |
Method 2: Using Application.DoEvents (okay)
To start, lets quickly run through what Application.DoEvents() actually does. From the framework, each time the form handles an event, it processes all the code associated with that event. All other events wait in a queue. While your code handles the event, your application does not respond. For example, the window does not repaint if another window is dragged on top.
If you call DoEvents in your code, your application will somewhat interrupt your code and handle the other events. For example, if you have a form that adds data to a ListBox and add DoEvents to your code, your form repaints when another window is dragged over it. If you remove DoEvents from your code, your form will not repaint until the click event handler of the button is finished executing.
With reference to the demo source code above, we first create an EventArgs object that basically stores the UI update data. So this EventArgs dutifully carries data from the task object to the UI object in each iteration.
Before the UI starts running the task, it pre-determines the updates that need that to be done by adding a new event to the task through AddEndCycleEvent. Note that Application.DoEvent is supposed to be called at the end the pre-determined set of updates to redraw the UI.
Finally, at the end of every iteration in the task object, it calls OnEndCycle(), which sends the EventArgs object back to the UI through the EventHandler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | // EventArgs to store your update data class NubcakeEventArgs : EventArgs { // ... } class MainForm { // ... void StartRunning(object sender, EventArgs e) { _task.AddEndCycleEvent(new NubcakeEventHandler( delegate(object nbSender, NubcakeEventArgs nbEvent) { // ... do all your UI stuff here. Application.DoEvents(); })); _task.StartEndlessLoop(); } } class WorkTask { event NubcakeEventHandler _endCycleEvent; readonly NubcakeEventArgs _eventArgs = new NubcakeEventArgs(); internal void StartEndlessLoop() { // ... while (i > -1) { // ... do task OnEndCycle(i++); // ... check to exit } } internal void AddEndCycleEvent(NubcakeEventHandler nbEvent) { _endCycleEvent += nbEvent; } void OnEndCycle(int val) { if (_endCycleEvent != null) { _endCycleEvent(this, _eventArgs.Update(val)); } } } |
Again, this method is very simple to use. But here’s the catch. The DoEvent method has a re-entry problem: Calling Application.DoEvents can cause code to be re-entered or re-performed if a message raises an event. In other words, this approach is not exactly multi-threading, but rather multi-tasking by switching to process event to event in the queue. Rightfully, long running tasks should be running in another thread, then we marshal calls to update the UI back to the UI thread. So DoEvents is considered a hacky solution to this problem because:
Application.DoEvents is taken straight from Delphis, and the same re-entry problem has been known for like 15 years now – Michael Starberg
(http://www.techtalkz.com/c-c-sharp/83924-application-doevents.html)
Instead, we next look at the .NET 2.0 BackgroundWorker class, which is pretty much better designed for such things. And here’s how its done.
Method 3: Using BackgroundWorker class (preferred)
Using the BackgroundWorker class is a multi-threading approach. Following the demo source code available at the top of this page, there are four main parts to this approach.
- WorkState is used to store the current working status of the task. It also stores the relevant data that will be used for updating the UI
- NubcakeWorker extends from BackgroundWorker, and this is the multi-threading processor. It contains three important event handlers namely DoWork, ProgressChanged, and RunWorkerCompleted. DoWork basically handles the event that starts the looping task, ProgressChanged handles the callback per iteration, and RunWorkerCompleted handles the event when the looping task ends or is terminated.
- MainForm is the UI that we want to update, and it contains two important methods. One is PerformTask that is invoked when a user clicks a start button. And the second is RefreshState that updates the UI, and it is called by the ProgressChanged method in NubcakeWorker per iteration.
- WorkTask is the task manager that performs the set of long running tasks. You can then update the UI by calling the worker.ReportProgress() function that invokes the ProgressChanged method to update the UI via RefreshState. That is ReportProgress -> ProgressChanged -> RefreshState -> UI updated.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | class WorkState { internal WorkState Start() { // Start state } internal WorkState Update() { // Update state } internal WorkState Cancel() { // Cancel state } internal WorkState End() { // End state } } class NubcakeWorker : BackgroundWorker { // Starts the INubCaketask with the endlessloop new void DoWork(object sender, DoWorkEventArgs e) { // ... e.Result = ((INubcakeTask)e.Argument).Start(worker); } // Invoked when a progress update is required new void ProgressChanged(object sender, ProgressChangedEventArgs e) { // ... // Param e.UserState contains a WorkState object that // contains all the UI update information. _parent.RefreshState(e.ProgressPercentage, e.UserState); } // Invoked when the task is completed or cancelled. new void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { _parent.CompletedTasks((bool)e.Result); } } class MainForm : Form, INubcakeForm { readonly INubcakeTask _task; // WorkTask readonly BackgroundWorker _worker; // BackgroundWorker void PerformTask(object sender, EventArgs e) { // starts the task. Calls event _worker.DoWork(); _worker.RunWorkerAsync(_task); } public void RefreshState(int progressPercentage, object userState) { // ... do the UI updates } } class WorkTask : INubcakeTask { public bool Start(BackgroundWorker worker) // i.e. NubcakeWorker { worker.ReportProgress(0, _state.Start()); while loop { // ... do task worker.ReportProgress(100, _state.Update()); // ... if cancelled, then report cancelled. } worker.ReportProgress(100, _state.End()); } } |
As you can see, BackgroundWorker needs a bit more work to get right. But once you get the hang of it, its a pretty neat way to separate your tasks from your UI. And, AFAIK its the encouraged practice. Personally, I think all the methods work. It really depends on your project requirements and risks.
Anyway, generally multi-threading or multi-tasking can help improve the responsiveness of the program, and CPU prioritization can also benefit application performance. However, don’t forget that threading has some computational switching costs. So if there are TOO many threads, then each thread may not be given enough time to execute much during its time slice. So, do use these approaches in moderation.
