The WaitableThread class
published: Fri, 18-Nov-2005 | updated: Fri, 18-Nov-2005
Continuing my occasional series on multithreading in C# and .NET
("continuing" in the obsessive sense of this just becoming a blog about
multithreading ), here's a quick implementation of a special kind
of thread, one that can be waited on. Hang on, I hear you say, that's
what
Thread.Join()
is
for isn't it?
The issue came up with the implementation of a design for another of our products. Essentially the developer had to implement a simple thread manager that ran some long-running code in different threads (say 20 or 30 of them). Because the code was interrogating other machines remotely (and they would in fact be doing most of the work and it was lengthy work), he didn't want to tie up the .NET thread pool.
Once a thread terminated he wanted to launch the next task as a
replacement thread. But the only way of waiting for a thread to finish
was to call Join()
and there were many threads from which
to choose. So he came to me for ideas.
The issue here is that, in Win32, it's relatively easy. You have a
thread manager that launches the first 20 threads and then calls
WaitForMultipleObjects()
on
the thread handles until one becomes signaled (that is, the thread
terminated). Each thread handle, in effect, is a waitable handle. At
that point you can set up another thread, launch it, and then call the
wait function again. A simple queuing mechanism in other words.
In .NET, a thread object is not a
WaitHandle
and
so you can't call WaitOne()
on an array of them. (The
WaitHandle
class contains the .NET versions of
WaitForMultipleObjects()
:
WaitHandle.WaitOne()
and
WaitHandle.WaitAll()
.). In fact, the CLR may, in its
wisdom, launch many .NET threads in the same Win32 thread. I seem to
recall some post about how on a 64-bit Windows instance, the CLR may
in fact use fibers rather than threads to host CLR threads. (This is
one reason the Thread
class doesn't have a Windows thread
ID property by the way.)
So the developer was wondering about having to create and maintain an
array of events (ManualResetEvent
or
AutoResetEvent
) and coordinating between the threads and
the events. The manager thread would wait on an array of event
objects. Each executing thread would signal its respective event
handle when it was done. This in turn meant that all threads had to
know about this mechanism and have code that called Set()
on the relevant object.
My response was to design and implement a WaitableThread
.
Here's the rather trivial code.
using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace JmBucknall.Threading { public class WaitableThread : WaitHandle { private ManualResetEvent signal; private ThreadStart start; private Thread thread; public WaitableThread(ThreadStart start) { this.start = start; this.signal = new ManualResetEvent(false); this.SafeWaitHandle = signal.SafeWaitHandle; this.thread = new Thread(new ThreadStart(ExecuteDelegate)); } protected override void Dispose(bool explicitDisposing) { if (explicitDisposing) { signal.Close(); this.SafeWaitHandle = null; } base.Dispose(explicitDisposing); } public void Abort() { thread.Abort(); } public void Join() { thread.Join(); } public void Start() { thread.Start(); } private void ExecuteDelegate() { signal.Reset(); try { start(); } finally { signal.Set(); } } } }
As you can see, it looks like a thread (it has a Start()
method and an Abort()
method and the like), so it's
source code compatible with a thread. However, it is not a thread
in the sense of it inheriting from the Thread
class, but
instead it inherits from WaitHandle
so it works with
WaitHandle.WaitOne()
. Here's an example of it in use.
using (WaitableThread enqueuerThread1 = new WaitableThread(new ThreadStart(enqueuer.Execute))) using (WaitableThread enqueuerThread2 = new WaitableThread(new ThreadStart(enqueuer.Execute))) using (WaitableThread dequeuerThread = new WaitableThread(new ThreadStart(dequeuer.Execute))) { WaitHandle[] handles = new WaitHandle[3]; handles[0] = enqueuerThread1; handles[1] = enqueuerThread2; handles[2] = dequeuerThread; Console.WriteLine("start the threads"); enqueuerThread1.Start(); enqueuerThread2.Start(); dequeuerThread.Start(); int finisherIndex = WaitHandle.WaitAny(handles); }
It's fairly limited for ordinary code since you can't use it wherever
a Thread
instance is required, but it solved this
particular issue admirably.