Background
I’ve previously discussed the differences between the BackgroundWorker and Thread classes, but I figured it would be useful to touch on some code. I’d like to share the pattern I commonly use when creating threads in C# and discuss some of the highlights.
The Single Thread
I like to use this design when I have a single thread I need to run and in the context of my object responsible for running the thread, I do mean having a single thread. Of course, you could have your object in control of multiple threads as long as you repeat this design pattern for each of them.
Here’s the interface that I’ll be using for all of the examples:
internal interface IThreadRunner { #region Exposed Members void Start(); void Stop(); #endregion }
Behold!
internal class SingleThreadRunner : IThreadRunner { #region Fields private readonly object _threadLock; private readonly AutoResetEvent _trigger; private Thread _theOneThread; #endregion #region Constructors /// /// Prevents a default instance of the class from being created. /// private SingleThreadRunner() { _threadLock = new object(); _trigger = new AutoResetEvent(false); } #endregion #region Exposed Members public static IThreadRunner Create() { return new SingleThreadRunner(); } public void Start() { lock (_threadLock) { // check if already running if (_theOneThread != null) { return; } _theOneThread = new Thread(DoWork); _theOneThread.Name = "The One Thread"; _theOneThread.Start(_trigger); } } public void Stop() { lock (_threadLock) { // check if not running if (_theOneThread == null) { return; } _theOneThread = null; _trigger.Set(); } } #endregion #region Internal Members private void DoWork(object parameter) { var currentThread = Thread.CurrentThread; // this was the trigger that we passed in. elesewhere in the // instance, we can use this object to wake up the thread. var trigger = (AutoResetEvent)parameter; try { // keep running while we're expected to be running while (currentThread == _theOneThread) { // DO ALL SORTS OF AWESOME WORK HERE. Console.WriteLine("Awesome work being done."); // put this thread to sleep, but remember it can be woken // up from other places in this instance. trigger.WaitOne(1000); } } finally { lock (_threadLock) { // if we were still expected to be running, change the // state to suggest that we're not if (_theOneThread == currentThread) { _theOneThread = null; } } } } #endregion }
This design was taken from some Java programming I had done in a previous life. Essentially, I have a thread that is responsible for doing some work in a loop. It could be anything… Periodically polling for some data, a work dequeing thread, a random-cursor-moving thread… Anything! The point is, you only want one of these suckers hanging around. How is this accomplished?
Leveraging the instance variable that marks the one expected running thread is key here. Whenever this thread checks if it should still be running, if the current thread doesn’t match what’s assigned to the instance variable then it needs to stop! This means you could potentially spawn off two of these threads, and if you set the instance variable to one of the two, then the other one should kill itself off! Pretty neat.
By using the reset event, we can actually interrupt this thread if it’s sleeping. This is great if we have a thread that periodically wakes up to do some work but we want to stop it and have it stop fast. We simple set our instance variable for the thread to be null and then set this thread’s reset event to ensure it get’s woken up. Presto! It wakes up, checks the condition, and realizes it needs to exit the loop.
Simple.
The Handful of Threads
This design is almost identical to the single thread design above. I use it primarily when I want to have an object responsible for a bunch of threads that are turned on/off under the same conditions. The major difference between the two designs? In the single thread scenario, we check that our current thread is still set to be the one instance. In this design, we need all of our threads to be checking against the same state object which is not going to be a single thread instance.
Let’s have a peek:
internal class GroupThreadRunner : IThreadRunner { #region Fields private readonly object _threadLock; private readonly Dictionary<Thread, AutoResetEvent> _triggers; private bool _running; #endregion #region Constructors /// /// Prevents a default instance of the class from being created. /// private GroupThreadRunner() { _threadLock = new object(); _triggers = new Dictionary<Thread, AutoResetEvent>(); } #endregion #region Exposed Members public static IThreadRunner Create() { return new GroupThreadRunner(); } public void Start() { lock (_threadLock) { // check if any are already running if (_triggers.Count > 0) { return; } _running = true; const int NUMBER_OF_THREADS = 4; for (int i = 0; i < NUMBER_OF_THREADS; ++i) { var thread = new Thread(DoWork); thread.Name = "Thread " + i; var trigger = new AutoResetEvent(false); _triggers[thread] = trigger; thread.Start(trigger); } } } public void Stop() { lock (_threadLock) { // check if not running if (_triggers.Count <= 0) { return; } _running = false; foreach (var trigger in _triggers.Values) { trigger.Set(); } } } #endregion #region Internal Members private void DoWork(object parameter) { var currentThread = Thread.CurrentThread; // this was the trigger that we passed in. elesewhere in the // instance, we can use this object to wake up the thread. var trigger = (AutoResetEvent)parameter; try { // keep running while we're expected to be running while (_running) { // DO ALL SORTS OF AWESOME WORK HERE. Console.WriteLine("Awesome work being done by " + currentThread.Name); // put this thread to sleep, but remember it can be woken // up from other places in this instance. trigger.WaitOne(1000); } } finally { lock (_threadLock) { _triggers.Remove(currentThread); // if we were still expected to be running, change the // state to suggest that we're not if (_running && _triggers.Count <= 0) { _running = false; } } } } #endregion }
Summary
The above patterns I discussed cover my common usage for threads: Instances that have reoccurring work over long periods of time. Both patterns are very similar and only have slight modifications to make them support one instance or many thread instances running. If you have one unique thread or many threads… there’s a pattern for you!
Check out a full working example of this code over here.