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.