Click here to Skip to main content
15,881,092 members
Articles / Programming Languages / C#
Article

C# Worker Thread Starter Kit

Rate me:
Please Sign up or sign in to vote.
4.49/5 (35 votes)
4 Jul 200417 min read 165.6K   2.6K   135   19
This article describes a simple pattern for worker threads and Form based programs.

Introduction

Here, I present a small class library and a set of examples for developing multithreaded programs based on Windows.Forms. This article focuses on three issues:

  • Synchronized sharing of data between the worker thread and main thread, which is the thread the Forms are run in.
  • Stopping the thread in a clean manner.
  • Safely firing events from the worker thread to the UI.

The examples here all use a shared data approach to communicate between the worker and the listeners, who in these examples are Forms. In this pattern, the worker writes results into the shared location, signals the listener, and goes on with its work. The listener at some later time responds to the signal and looks at the shared location for anything that may interest it. Because data flow is largely one direction, from the worker thread to the owner or listener, this pattern resembles a producer-consumer using a mailbox. The examples demonstrate how to synchronize access to the shared data.

If you were so inclined (evil), you can put code that takes hours to complete in an OnClick event handler. Such a design will kill the user interface, UI, for the duration of the task, and while it is a bad design, is not wrong, i.e. it will work. With this bad design, the user will have to Ctrl-Alt-Del to stop your program. The user frustration such designs provoke will bring you a lifetime of bad karma. One of the advantages of moving a long task into a worker thread is that the UI can contain a Stop button. With the task in a separate thread, you do not kill the UI that runs in the main thread, and a Stop button will be operable at the same time the thread is running. (Please, nobody comment about using PeekMessage or other message queue monstrosities.) In the examples, I have tossed in a RunStopButton control that you might find useful in your projects.

With the background task now executing in a worker thread, and a Stop button on the form, we are able to stop the worker thread. There are two approaches to stopping a worker, cooperatively or with malice. The examples show a common approach to cooperatively stop a thread that uses two signals: one from the owner requesting the thread to stop, and a second from the thread acknowledging that it has stopped. The method of terminating a thread with malice is the Abort() method provided by the .NET API. The framework provides an ability for the thread to detect the Abort and try to exit cleanly, but I still judge this method to be only a fallback to use when the thread refuses to cooperate.

The main stumbling point to using WinForms and threads together is a critical issue about the UI: only the thread that creates a form can use the form. Commonly, the main thread creates all your forms, which implies that only the main thread can access any of the forms. At first glance, this seems to defeat one of the main uses of threads: often you split long execution time sections of code out of the UI into worker threads to keep the UI alive. The key to this problem is how to fire events from one thread back into the main thread. There are other articles discussing this detail, here I just implement in a reusable class.

Note: if you try to access a Form from another thread, you may get lucky and see your code work as you expected and contrary to the exclusion stated in the docs. Rest assured if you are such a lucky person, that luck will expire at the most inopportune moment (see Murphy's Law: Whatever can go wrong, will go wrong, and at the worst possible moment).

The Class Library

The project H5ThreadLib builds a class library for use in multithreaded programs. The Hangar5.Threading namespace contains the few classes used as the basis for all the examples. There are two main sections in this library: the thread and the controller. The UML diagram below outlines the first section.

Image 1

ThreadBase is an abstract class to use as a base for all threads and uses a standard event/delegate for signaling changes or progress. StopRequestException is a custom exception used to indicate the cooperative request for the thread to stop. SharedData is a base class for providing synchronized access. The ThreadBaseData class provides some standard shared data members for ThreadBase.

ThreadBase

To make a worker thread using this library, you make a derived class from ThreadBase. To implement your class, you just implement Run() similar to the Java Thread class. The base class implements some standard start/stop behavior in RunOuter that is required by the controllers discussed in the next section. To be well behaved, your Run() method only must do two things:

  • call FireStatusTick whenever changes have occurred that you would like other objects to know about.
  • use Wait() instead of Sleep().

The threads use only the one event for all signaling. The event contains very little data, mostly the current running/stopped state of the thread. It is intended to be just a notification that changes are available in the SharedData and that something may want to come and take a look. As such, this event does not need to be redefined for different applications.

The Wait method in the ThreadBase class is the key to stopping your thread in a cooperative manner. It should be used anyplace Sleep() would be used and sprinkled within long loops. The method is implemented as a WaitOne call on a ManualResetEvent:

C#
protected void Wait( int nMs ) {
     if( basedata.signalStopRequest.WaitOne( nMs, false ) ) {
         throw new StopRequestException();
     }
     return;
 }

The signalStopRequest is the event that the controller will set if some other party requests the thread to stop. The usage here is backwards from typical in that mostly we expect that the timeout limit will be reached because the event was not set. Thereby, the WaitOne call will block for the timeout period and then return false. If the event is set on entry to the method or is set anytime during the WaitOne call, the exception is thrown to interrupt the worker thread.

If your thread code is nothing but a huge calculation loop, you can scatter in calls to Wait() to allow the thread to be interrupted. For example:

C#
const int chunk = 1000;
int i, j;
i = 0;
while( i < 1000000 ) {
  Wait(0);
  for( j=0; j<chunk; j++ ) {
    /* do something */
    i++;
  }
}

It is up to you to determine how frequently to check. The basis for the decision is knowing how patient your users are. Even if you place no Wait() calls, the terminate with malice approach will still be able to stop your thread.

The base class contains standard behavior for the StopRequestException, so your code does not have to contain a catch clause for the exception. If there is something particular your code wants to do when StopRequestException is thrown, it can catch this exception and is required to re-throw it. The examples demonstrate this.

Just using the ThreadBase class has only addressed part of the issues: stopping the thread is half implemented and the beginnings of the notification scheme are implemented. But the events thrown here have the problem discussed in the Introduction.

SharedData

In the pattern I use in this article, all data members that may be shared with other threads are factored out into a separate class. You could write these as members with public accessors of the thread class itself. I use a class derived from SharedData largely as a way of focusing attention on the data members that may be shared and require synchronization. Any members that will be shared between the worker and other threads are placed in this object and need to:

  1. implement synchronized access with the aid of the class members
  2. be ruthlessly inspected to ensure that they do not require synchronization

It is so trivial to synchronize access, that it is hard to imagine a situation where you would use b). The separate class for the shared data members also provides a useful design flexibility. The owner of the worker thread can retain the shared data after the worker is done. You can think of this as a teacher giving a quiz to a student, at the end of the period the student gives back the quiz and leaves, and the teacher still has the quiz.

For each thread class you write, a matching thread data class derived from SharedData will be required. The thread data class will contain probably just properties and accessor methods. To synchronize access to the data, use a matched pair of calls, ReaderLock()/ReaderRelease() for getter methods, and WriterLock()/WriterRelease() for setter methods. For example:

C#
public class ThreadXData : SharedData {

    public string Message {
        get {
            string sRet;
            ReadLock();
            sRet = sMsg;
            ReadRelease();
            return sRet;
        }
        set {
            WriteLock();
            sMsg = value;
            WriteRelease();
            return;
        }
    }

    protected string sMsg = "";
}

shows a simple thread data class with one string property. To get the string, lock it, make a copy, and unlock it. The read lock will prevent any writing to the string while the copy is made. To set the string, lock it, write the value, and unlock. Because this pattern is common, the base class has convenient functions that reduce the typing to:

C#
public class ThreadXData : SharedData {

    public int Complete {
        get {
            return LockedCopy( ref nPercent );
        }
        set {
            LockedSet( ref nPercent, ref value );
        }
    }

    protected int nPercent = 0;
}

The examples demonstrate a few other types of accessor functions and the same lock/release usage.

A key simplicity to the SharedData class is that only one mutex/lock is used for access to all the members. One common example of deadlocks in multithreaded code is blocking on two or more mutexes: one thread locks B and is interrupted, a second thread locks A then tries to lock B and is blocked, the first thread continues and tries to lock A resulting in a deadlock (this is the copy operator problem). By using just one lock in SharedData, the complexity of the cases that you have to consider is greatly reduced.

Be aware that this deliberate coarse grain access for the entire SharedData class means that all the members are locked at once. So, if the worker is writing to variable x, the owner cannot read variable y. You may worry that threads will be unnecessarily blocking waiting for access to the data members and your program will suffer a performance hit. I would argue that in this pattern, the probability of that are low for two reasons. First, these examples are talking about user interface updates - not game development. There is no reason to have screen updates occur at frequencies above a few times a second, if even that. The duration of holding a lock should be very low. All you are doing in the data member accessors is an assignment and return. With GHz processors, an assignment x=bla should take a fraction of a microsecond. Comparing the frequency of the UI updates with the interval the locks are held, the probability two threads will collide is low. Second, the nature of worker threads and UIs tend to be largely producer consumer patterns. The events loosely serialize the actions such that the worker writes some results and the UI comes along and reads the results. For applications where these two cases hold true, a single lock will perform well.

Hopefully, the SharedData class and the examples show that providing synchronized access to data is quite straightforward. To be complete, the library needs one more piece to handle the issue with events.

Controllers

The diagram below shows the two controller classes that are provided to control the thread and to handle the events from the thread.

Image 2

These two classes can control any class derived from ThreadBase. They provide the typical Start() method and a few stop methods:

  • Stop - uses the cooperative method of stopping the thread provided by the base class.
  • Abort - uses the .NET API Abort method to terminate the thread with malice.
  • Terminate - stops the thread by first trying Stop, and if the worker thread is not cooperative, tries Abort.

The interesting role of the controllers is to forward the events from the thread to other destinations, referred to as listeners. In this library, listeners must be Controls and typically are Forms. The SingleSink controller accepts only one listener and the MultiSink controller allows for any number of listeners. Listeners register themselves with the controller by providing a Form and a delegate. In both cases, the controllers receive the event from the worker thread and forward to the listeners.

BeginInvoke

Although you may want to, you cannot have a thread fire events through the standard event/delegate approach. This limitation is imposed by the Windows.Forms framework that requires that only the thread that creates a Form can use it. This same limitation exists in C++ and MFC applications, so it is nothing new. For example, if you where to fire an event from a worker thread to a delegate in a Form to change some text, that delegate would be executing in the context of the worker thread. BeginInvoke() is the solution to this problem as it does not immediately execute the delegate, but instead causes a message to be placed in the receiver's message queue. The message receiver then, at a later time, executes the delegate in its own thread context. For those with a Win32 API background, this solution is the same as using a PostMessage to put an event into another window's queue. The FireStatusTick methods in the two controllers are wired to the status tick event in the worker thread, and forward the event to the listeners in the DoEvent() method in ControllerBase.

Why Not Invoke?

You can use Invoke. I have seen other articles that suggest that you can use Invoke() and that the Delegate will execute in the same context as the thread that created the Delegate. You can explore this option by editing the DoEvent(). Indeed, if you set breakpoints, you will see that Invoke will cause the needed context switch to correctly execute the Delegate. Again, referring back to the standard Win32 API, this is an analog to SendMessage in that the Delegate is executed immediately and a return value is available. For the pattern used in this article, there is no use for the return value because the message is a one direction signal. Also, the hard synchronization provided by Invoke is of no benefit.

By chaining the event from the thread to the user interface, the controller classes finish the last of the three issues outlined in the introduction. Time to see it all in action.

The Big Picture

The associations between these classes are outlined in the figure below.

Image 3

Yikes...

Wait, it's simple. The shaded classes are what you write for your application. MyControl, typically a Form, is the UI element that will be the listener for some aspect of the thread's results. MyThread contains the worker thread for your application and MyThreadData is the matching SharedData. The UI element consists in three main classes of activities: it controls the thread using the type of ControllerBase it created (mostly start and stop), it receives signals from the thread via StatusTickHandlers, and it shares data via MyThreadData. The examples contain samples of how to do this.

The Examples

Enough with the diagrams (all made with Dia by the way), time for some stuff that runs. The source code download includes four sample applications to demonstrate various aspects of the class library.

The sample applications contain a Run/Stop button and a Busy button. The Busy button is used to simulate something that would make the UI unresponsive for about one second. The simulation is accomplished with a Sleep(1000) that would never be in real applications. A real operation like saving a file might reasonably make the main thread busy for a second.

Test 0 - Timer Based Polling

This example does not fire events to the listeners, instead it relies on the main thread to poll at a regular interval for the results. This approach can be lightly described:

Boss: Worker bee, here is a task for you, go do it. 
    Just check off these boxes
    on the white board as you finish each part. 
Worker bee: [shuffles off to cube] 
[time passes] 
Worker bee:  [checks off first box] 
[time passes] 
Boss: [looks at white board sees 1 box checked]Ahh good 
[time passes] 
Boss: [looks at white board sees 1 box checked]mutter mutter
[one second latter] 
Worker bee: [checks off second box - Boss does not see] 
[time passes] 
Worker bee: [checks off third box] 
Boss: [looks at white board sees 3 boxes checked] 
      Wow this is flying. I'm gonna get a bonus.
[time passes] 
Boss: [looks at white board sees 3 boxes checked]What? Nothing happened. 
     I got out of my chair just to see nothing happening. 
     (bellows)Worker bee! When will box 4 be done?      
Worker bee: I just finished it, if you had waited just 1 second
      more, you would have seen it.

This example starts a worker thread that creates a report containing several sections or steps. The worker makes the cumulative sections of the report available as SharedData. The owner Form sets up a timer and polls the cumulative result from the worker.

When you run this example, you will see that lack of synchronization. The time intervals in this example have been forced to demonstrate that the update of the screen is not well synchronized with the activity of the worker thread. You should see that several of the steps executed by the worker are not displayed. Even though the changes along the way are not accurately displayed, the net result displayed in the text box is complete. This aesthetic UI issue can be resolved by increasing the frequency that the UI polls. While that approach may make the UI respond well, the application starts to waste time updating the screen frequently with data that has not changed.

While it may not be elegant, this approach is direct and safe. To improve things, the next example uses notification from the worker thread that indicates when changes occur. That implementation will allow the UI to only update when changes are required, and will tighten up the synchronization between the moment the changes occur and when the changes are made visible in the UI.

Test 1 - Putting it All Together

This program is the same as Test 0 except that it uses all the features of the library. You should see the report update regularly. Nonetheless, if you press the Busy button, the UI will stop updating for a second.

Test 2 - Multi Sink Example

This example demonstrates an application using multiple sinks for the thread status ticks. When the thread is running, a modeless dialog pops up showing a progress bar. When the thread finishes, the dialog hides.

This application demonstrates one of the advantages of separating the data out of the signals. It is possible to write the application to place all the result data into the event arguments. But this application shows the inefficiency such an approach causes. The data each Form received in the events would be mostly of no interest. With the pattern in this library, the Form's fetch from the 1 shared object, only what is of use to them.

Test 3 - The Data Fountain

One way of looking at the pattern I use here is like a communications connection where our SharedData is the in-band data, and the events are out-of-band control data. This is the model I have in mind that keeps me from putting all the data produced by a worker into the event arguments. I like separating the state/SharedData from the path/Events. At first glance, a worker thread that produces a continuous stream of results may not fit this pattern. Telemetry data would be a hard example, where the worker thread produces a chunk of data at regular intervals. You may be tempted to pack the chunk of data into the EventArgs.

Test 3 shows the solution I prefer that uses a Queue in the SharedData for the worker thread to store the chunks of results it produces. Instead of firing the entire chunk of data to a listener through EventArgs, it stores the results and just signals the listener that there is data available. The Queue ensures that the listener will receive one or more chunks in the order they where produced.

Summary

Somewhere I read a statement about threading that warned "if it is hard, you are doing it wrong". That rings true to me, and maybe the examples and library in this article support that statement. I have long used multiple threads in C++ programs to simplify the design. While some people seem to warn developers away from threads, I would encourage you with the claim that a multi-threaded program is simpler than the non-threaded program that accomplishes the same functionality. This was my first C# project, and resulted from translating a pattern I had long used in C++. Many of the bits and pieces are thoroughly discussed in other articles, here, and on MSDN. Mostly, here I just wanted to bring everything together. Hopefully, this library and examples will provide a good starter kit for other beginners like me.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Systems Engineer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionOleDb and DB2 Pin
wallyballs19-Jun-06 16:29
wallyballs19-Jun-06 16:29 
QuestionAny idea how to control multiple worker threads??? Pin
nalla5-May-06 13:44
nalla5-May-06 13:44 
GeneralClose down app needs to stop thread Pin
DanBaker23-Mar-05 12:24
DanBaker23-Mar-05 12:24 
QuestionWhy not using the Thread.Interrupt? Pin
HarveyKwok29-Jan-05 20:23
HarveyKwok29-Jan-05 20:23 
AnswerRe: Why not using the Thread.Interrupt? Pin
James Rudnicki1-Feb-05 20:10
James Rudnicki1-Feb-05 20:10 
GeneralRe: Why not using the Thread.Interrupt? Pin
HarveyKwok1-Feb-05 20:26
HarveyKwok1-Feb-05 20:26 
GeneralRe: Why not using the Thread.Interrupt? Pin
James Rudnicki1-Feb-05 22:18
James Rudnicki1-Feb-05 22:18 
GeneralRe: Why not using the Thread.Interrupt? Pin
HarveyKwok2-Feb-05 4:53
HarveyKwok2-Feb-05 4:53 
GeneralRe: Why not using the Thread.Interrupt? Pin
Al Forno23-Mar-05 12:02
Al Forno23-Mar-05 12:02 
AnswerRe: Why not using the Thread.Interrupt? Pin
Hypnotron15-Jun-07 17:14
Hypnotron15-Jun-07 17:14 
GeneralQuestions: ReaderWriterLock vs. lock, Aborting thread Pin
NutsEater9-Jul-04 5:33
NutsEater9-Jul-04 5:33 
Very attractive tool, but I want to avoid some coding styles (for example, public fields) so I am trying to rewrite classes, but...

I recognize some problems that

(1) ReaderWriterLock vs. lock
ShardData uses ReaderWriterLock instead of lock statement, so I think that without volatile declaration the fields may be obsolete even if worker thread’s final signal are received. But, it seems foolish that all fields are declare with violate keyword, because it causes losing performance though ReaderWriterLock is used instead of lock to gain performance.

(2) Aborting thread
Aborting thread often makes me confused, because it is difficult for me to consider all situations when abort is requested. For example, how about the situation that worker thread is normal exiting and now go into finally clauses but just before casting final signal? I think that aborting in that situation makes no signal fired and the thread being dead.
Also I face problems when replacing ManualResetEvent (extending WaitHandle) to Monitor or something because I heard WaitHandle is expensive. It uses OS resource and has dependency to OS. But aborted thread can wait to get lock to pulse? How about thread is interrupted while waiting? (Now, I am thinking that it is better to use ThreadPool etc. to callback.)

By the way, I think atomic operations such as substitutions are not needed to be synchronized except problem about using or not using volatile keyword.

--
Hirohsi Ikeda
GeneralRe: Questions: ReaderWriterLock vs. lock, Aborting thread Pin
James Rudnicki9-Jul-04 9:45
James Rudnicki9-Jul-04 9:45 
GeneralRe: Questions: ReaderWriterLock vs. lock, Aborting thread Pin
salihg13-Dec-05 4:53
salihg13-Dec-05 4:53 
GeneralRe: Questions: ReaderWriterLock vs. lock, Aborting thread Pin
James Rudnicki9-Jul-04 10:02
James Rudnicki9-Jul-04 10:02 
Generalgood stuff! Pin
Radeldudel7-Jul-04 21:16
Radeldudel7-Jul-04 21:16 
GeneralRe: good stuff! Pin
James Rudnicki9-Jul-04 10:09
James Rudnicki9-Jul-04 10:09 
Generalresx not included in zip Pin
Martin Hart Turner29-Jun-04 22:40
Martin Hart Turner29-Jun-04 22:40 
GeneralRe: resx not included in zip Pin
James Rudnicki9-Jul-04 10:03
James Rudnicki9-Jul-04 10:03 
QuestionWhat about a &quot;Work Class&quot; Pin
trevor_ledet17-Jun-04 15:03
trevor_ledet17-Jun-04 15:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.