Click here to Skip to main content
15,671,149 members
Articles / General Programming / Threads
Article
Posted 13 Nov 2015

Stats

17.2K views
197 downloads
19 bookmarked

A Swiss Army Knife Thread Support Class

Rate me:
Please Sign up or sign in to vote.
4.89/5 (8 votes)
13 Nov 2015CPOL7 min read
Make .NET threadpool threads jump through hoops with this handy support class

Output from WorkerThreadController test driver

Introduction

Threads are the unsung heroes of modern apps. They toil in the background with little recognition, dilligently going about their business, performing the mundane tasks that allow apps to be so flexible and responsive. If you could see them, they would probably look like those zany little Minions from Despicable Me. And like Minions, once they're off and running, they can be difficult to keep under control. Sure, you can join them, and even abort them, but can you get them to sit up, roll over, and deal blackjack? That's more or less what this article is going to show you how to do.

Background

How many ways are there to create a thread? At least ten, according to this article. While some of them provide interesting and useful features, you can still get a lot of mileage from dull but reliable QueueUserWorkItem. The question this article addresses is what to do with those threads once you turn them loose. You know about CancellationTokenSource and ManualResetEvent (I'll review them below just in case), but if you use one, you can't use the other without some extra programming. You know that join is probably a good thing, abort is usually a bad thing, especially when it comes to thread pool threads, and exceptions never get out of the thread alive. .NET Threads just cry out for simplified management. Tasks were introduced in .NET 4.0 (and enhanced in 4.5) to address many of these issues, so you might want to explore what they have to offer if you haven't already. But if what you want is a simple threadpool thread with some enhanced features, keep reading.

The Wish List

At one time or another I've needed each of these capabilities either alone or in combination:

  1. Wait for a worker thread to complete (join)
  2. Time out a worker thread, and detect that a timeout has occurred
  3. Cancel a worker thread (with optional timeout on the cancel)
  4. Catch exceptions thrown by the worker thread
  5. Return a status code and description
  6. Send in arbitrary data
  7. Return arbitrary data
  8. Get periodic status from the worker thread

It would be nice if there was minimal explicit support required in new or existing WaitCallback methods to support these features. And of course, we want to support all of them simultaneously.

The Solution

This article describes a C# class named WorkerThreadController which does double duty as both a wrapper/manager for the user's WaitCallback method, and a data package for both the user and the wrapper WaitCallback methods. The class and a test driver program in a VS2010 solution can be downloaded using the link at the top of this page. Let's begin with a look at the constructor:

/// <summary>
/// Constructor.  Intitialize and then start a worker thread using our
/// internal wrapper method.  This (WorkerThreadController) object is
/// passed as data into the wrapper method.
/// </summary>
/// <param name="method">The WaitCallback method as far as the user is concerned</param>
/// <param name="data">Optional data to be supplied to the user's method</param>
public WorkerThreadController(WaitCallback method, object data = null)
{
	DataIn = data;
	cancellationToken = new CancellationTokenSource();
	resetEvent = new ManualResetEvent(false);
	userMethod = method;
	ThreadPool.QueueUserWorkItem(UserMethodWrapper, this);
}

CancellationTokenSource provides a mechanism that allows a parent to request cancellation of a worker thread by calling CancellationTokenSource.Cancel(). The worker thread must actively check for and act upon the cancellation request by polling CancellationTokenSource.IsCancellationRequested. CancellationTokenSource also provides some more sophisticated options which are beyond the scope of this article. For details, check out this MSDN blog

ManualResetEvent has been described as a "door" that one thread waits to enter, while another thread opens and closes the door. One way to implement a join operation is to use a ManualResetEvent, where the parent waits for the door to open using a call to ManualResetEvent.WaitOne(), and the worker thread opens the door when it exits using a call to ManualResetEvent.Set().

The WorkerThreadController is going to manage all the details of the CancellationTokenSource and ManualResetEvent for you. The wrapper method to be run in the worker thread exists primarily to intercept exceptions thrown by the user's WaitCallback method, and store them to be rethrown later in the parent thread. As a parting shot, it will set the ManualResetEvent (i.e., allow the waiting parent to continue execution). Here's what the wrapper method looks like:

/// <summary>
/// Wrap the user's method to catch exceptions and set the resetEvent.
/// This allows the parent to continue if it was waiting.
/// </summary>
/// <param name="data">A reference to this object</param>
private void UserMethodWrapper(object data)
{
	//
	// OK is default true unless explicitly set otherwise in the worker
	// thread.  That way, the worker thread doesn't need to bother 
	// setting it true in the happy path case.  Even if an exception
	// is thrown, don't assume OK is false; let the userMethod set it
	// if it wants to.
	//
	try
	{
		userMethod(data);
	}
	catch (Exception ex)
	{
		if (TransferExceptions) TheException = ex;
	}

	//
	// The WaitCallback wrapper is about to exit.  Let the parent know
	// in case it's waiting on the ManualResetEvent.
	//
	resetEvent.Set();
}

The following sections demonstrate how WorkerThreadController can be used to implement each of the features in the wish list. The highlighted code blocks contain a code fragment run by the parent thread, followed by the user's WaitCallback method, and finally the console output produced when the code is executed.

I Hope Someday You'll Join Us (#1)

In this scenario, the parent thread will create a WorkerThreadController using Worker1 as the WaitCallback method, and then wait for the worker thread to complete execution. Nothing fancy, just a simple example of using a WorkerThreadController.

	//-----------------------------------------------------------------
	// Test 1: Simple happy path.  The thread works a bit, then exits.
	//-----------------------------------------------------------------
	Console.WriteLine("Starting Test 1, simple happy path");
	WorkerThreadController workerThread1 = new WorkerThreadController(Worker1);
	workerThread1.WaitForever();


static void Worker1(object data)
{
	Console.WriteLine("  Worker1 is starting");
	Thread.Sleep(250);
	Console.WriteLine("  Worker1 is exiting");
}

Worker1 output

This Is Taking Forever (#2)

We can't always wait forever, so this time we're going to set some limits. The setup is the same as described for #1 above, but instead of waiting forever, we're going to cap the wait at 300ms. If the worker thread is still running at that point, we're going to cancel it. We're going to assume that the worker thread is behaving nicely (even if it has exceeded its alotted time), and that it will respond reasonably promptly to a cancel request. It's not shown in this example, but WorkerThreadController.ThrowCancelException gives you the option of throwing an OperationCanceledException when the worker thread is canceled.

	//-----------------------------------------------------------------
	// Test 2: Timeout.  The worker is taking too long, so kill it.
	//-----------------------------------------------------------------
	Console.WriteLine("Starting Test 2, worker join timeout and cancel");
	WorkerThreadController workerThread2 = new WorkerThreadController(Worker2);
	if (workerThread2.IsStillRunningAfter(300))
	{
		Console.WriteLine("  >>> Timeout! Cancel the worker");
		workerThread2.Cancel();
	}

	
static void Worker2(object data)
{
	WorkerThreadController controller = (WorkerThreadController)data;
	Console.WriteLine("  Worker2 is starting");
	for (int i = 0; i < 50; i++)
	{
		Console.WriteLine("  Worker2 is working...");
		Thread.Sleep(100);
		if (controller.IsCanceled())
		{
			Console.WriteLine("  Worker2 has been canceled");
			break;
		}
	}
	Console.WriteLine("  Worker2 is exiting");
}

Worker2 output

OK, I've Had Enough, And This Time I Mean It (#3)

What if the worker thread is not behaving nicely? We don't really want to wait forever for the thread to be cancelled. What to do? WorkerThreadController gives you the option of placing a timeout on the cancel request. If the cancel times out, that means the worker thread is refusing to cooperate, or maybe it can't because it's stuck waiting on some other thread. In any case, your options are limited. You've already waited as long as you can. You could abort the thread, but as you know that can cause problems. Another option would be to log as much information as possible and shut down the app. Or, you could just ignore the problem and hope for the best. You'll have to decide what's right in your situation. In the example below, the worker thread does eventually exit on its own.

	//-----------------------------------------------------------------
	// Test 3: Cancel.  The worker ignores the cancel.
	//-----------------------------------------------------------------
	Console.WriteLine("Starting Test 3, worker ignores the cancel");
	WorkerThreadController workerThread3 = new WorkerThreadController(Worker3);
	Thread.Sleep(200);
	if (!workerThread3.Cancel(100))
	{
		Console.WriteLine("  >>> The cancel timed out!");
	}
	Console.WriteLine("  End of Test 3");
	Thread.Sleep(300);  // the worker will quit eventually

	
static void Worker3(object data)
{
	WorkerThreadController controller = (WorkerThreadController)data;
	Console.WriteLine("  Worker3 is starting");
	for (int i = 0; i < 5; i++)
	{
		Console.WriteLine("  Worker3 is working...");
		Thread.Sleep(100);
		//
		// Ignoring the cancellation here...
		//
	}
	Console.WriteLine("  Worker3 is exiting");
}

Worker3 output

I Didn't See That Coming (#4)

It's often the case that an exception is better handled in the parent thread, where there may be enough information to correct a problem and start the worker thread over again. WorkerThreadController supports this feature by catching and recording the exception in a wrapper around the user's WaitCallback method, then (optionally) re-throwing the exception in the parent thread when the parent waits for the worker thread to complete. You can choose what class of exception to throw from your WaitCallback method; you might prefer something more specialized than Exception.

	//-----------------------------------------------------------------
	// Test 4: Exception.  Catch the worker's exception.
	//-----------------------------------------------------------------
	Console.WriteLine("Starting Test 4, exception");
	try
	{
		WorkerThreadController workerThread4 = new WorkerThreadController(Worker4);
		workerThread4.WaitForever();
	}
	catch (Exception ex)
	{
		Console.WriteLine("  Exception: {0}", ex.Message);
	}

	
static void Worker4(object data)
{
	WorkerThreadController controller = (WorkerThreadController)data;
	Console.WriteLine("  Worker4 is starting");
	throw new Exception("An exception in Worker4 has occurred!");
}

Worker4 output

How Did The Story End? (#5)

Sometimes all you need is a status code and possibly a description if something went wrong. The worker thread need only set the OK and Details fields before returning. The parent thread can then access them. You could of course replace bool OK with a numeric, enumerated, or string code if that better suits your needs.

	//-----------------------------------------------------------------
	// Test 5: Simple failure with status code and details.
	//-----------------------------------------------------------------
	Console.WriteLine("Starting Test 5, status code and details");
	WorkerThreadController workerThread5 = new WorkerThreadController(Worker5);
	workerThread5.WaitForever();
	Console.WriteLine("  Status: {0}", (workerThread5.OK) ? "OK" : "FAIL");
	Console.WriteLine("  Details: {0}", workerThread5.Details);

	
static void Worker5(object data)
{
	WorkerThreadController controller = (WorkerThreadController)data;
	Console.WriteLine("  Worker5 is starting");
	Thread.Sleep(250);
	controller.OK = false;
	controller.Details = "A problem occurred during processing!";
	Console.WriteLine("  Worker5 is exiting");
}

Worker5 output

Show Me Yours And I'll Show You Mine (#6 and #7)

If the worker thread does any sort of complex processing, it will likely need some complex input from the parent thread, and may need to return complex output when it's done. DataIn and DataOut allow you to pass any arbitrary data into and out of the WaitCallback method. The WaitCallback method is actually free to use either, both, or neither of these fields, in any way you choose. The names simply suggest their use. Additional fields can be added.

	//-----------------------------------------------------------------
	// Test 6-7: Send in some data, get some data back out.
	//-----------------------------------------------------------------
	Console.WriteLine("Starting Test 6-7, data in and data out");
	WorkerThreadController workerThread67 = new WorkerThreadController(Worker67, "PURPLE COWS");
	workerThread67.WaitForever();
	Console.WriteLine("  Status: {0}", (workerThread67.OK) ? "OK" : "FAIL");
	Console.WriteLine("  Returned data: {0}", workerThread67.DataOut);

	
static void Worker67(object data)
{
	WorkerThreadController controller = (WorkerThreadController)data;
	Console.WriteLine("  Worker67 is starting");
	Console.WriteLine("  Worker67 input data: {0}", controller.DataIn);
	Thread.Sleep(250);
	controller.DataOut = 3.14159;
	Console.WriteLine("  Worker67 is exiting");
}

Worker67 output

Wazzup? (#8)

While the worker thread is running, especially if it's a lengthy operation, you might want to know its current status. WorkerThreadController provides a thread-safe Status property that can be set in the worker thread, and retrieved in the parent thread. I suppose it would work in the opposite direction as well if you found a use for it. One way to take advantage of this feature would be to use a periodic timer to check the status, then update your GUI with the results, for example using a progress bar.

	//-----------------------------------------------------------------
	// Test 8: Status.  Check thread status.
	//-----------------------------------------------------------------
	Console.WriteLine("Starting Test 8, check status");
	WorkerThreadController workerThread8 = new WorkerThreadController(Worker8);
	for (int i = 0; i < 3; i++)
	{
		Thread.Sleep(100);
		Console.WriteLine("  >>> Status: {0}", workerThread8.Status);
	}
	workerThread8.WaitForever();

	
static void Worker8(object data)
{
	WorkerThreadController controller = (WorkerThreadController)data;
	Console.WriteLine("  Worker8 is starting");
	for (int i = 0; i < 2; i++)
	{
		Console.WriteLine("  Worker8 is working...");
		controller.Status = "working";
		Thread.Sleep(100);
	}
	controller.Status = "done";
	Console.WriteLine("  Worker8 is exiting");
}

Worker8 output

It's Your Turn

And that's it. I hope you found this article interesting, and possibly even useful. You might be happy with WorkerThreadController just the way it is, or you might already be thinking of some enhancements you'd like to see. Perhaps a status callback delegate instead of a simple string, or maybe WaitCallback method chaining. Let me know what clever modifications you make.

History

Initial version 13 November 2015

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Qodex Software
United States United States
I am a software architect, engineer, consultant, and educator based in Dallas. Projects and engagements have ranged from weather radar to Unix kernel to robotic controllers to enterprise financial systems, for a number of Well Known (and some not) companies.

I've been doing this since Z80s and CP/M were bleeding edge. After I outgrew FORTRAN, I had flings with a dozen languages; my current infatuation is C# and .NET.

I have a diverse and lengthy academic background, much of it in the area of Computer Science. Now I give back by teaching C++ and C# to impressionable youth at a Texas college.

Comments and Discussions

 
QuestionTPL Pin
William E. Kempf17-Nov-15 3:03
William E. Kempf17-Nov-15 3:03 
QuestionLacking pause Pin
Member 1159380817-Nov-15 2:31
Member 1159380817-Nov-15 2:31 
AnswerRe: Lacking pause Pin
William E. Kempf17-Nov-15 3:46
William E. Kempf17-Nov-15 3:46 
GeneralRe: Lacking pause Pin
Member 1159380817-Nov-15 4:45
Member 1159380817-Nov-15 4:45 
GeneralRe: Lacking pause Pin
Tim Bomgardner17-Nov-15 8:47
professionalTim Bomgardner17-Nov-15 8:47 
GeneralRe: Lacking pause Pin
William E. Kempf18-Nov-15 4:30
William E. Kempf18-Nov-15 4:30 
GeneralRe: Lacking pause Pin
Member 1159380818-Nov-15 4:43
Member 1159380818-Nov-15 4:43 
GeneralRe: Lacking pause Pin
William E. Kempf18-Nov-15 6:18
William E. Kempf18-Nov-15 6:18 
GeneralRe: Lacking pause Pin
Member 1159380818-Nov-15 21:40
Member 1159380818-Nov-15 21:40 
GeneralRe: Lacking pause Pin
William E. Kempf19-Nov-15 2:15
William E. Kempf19-Nov-15 2:15 
GeneralRe: Lacking pause Pin
Member 1159380819-Nov-15 3:07
Member 1159380819-Nov-15 3:07 

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.