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:
- Wait for a worker thread to complete (join)
- Time out a worker thread, and detect that a timeout has occurred
- Cancel a worker thread (with optional timeout on the cancel)
- Catch exceptions thrown by the worker thread
- Return a status code and description
- Send in arbitrary data
- Return arbitrary data
- 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:
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:
private void UserMethodWrapper(object data)
{
try
{
userMethod(data);
}
catch (Exception ex)
{
if (TransferExceptions) TheException = ex;
}
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
.
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");
}
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.
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");
}
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.
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);
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);
}
Console.WriteLine(" Worker3 is exiting");
}
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
.
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!");
}
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.
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");
}
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.
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");
}
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.
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");
}
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