The source code accompanying this book contains the WorkerThread
class, which is a high-level wrapper class around the basic .NET Thread
class. WorkerThread
is defined as:
public class WorkerThread : IDisposable { public WorkerThread(); public WorkerThread(bool autoStart); public int ManagedThreadId{get;} public Thread Thread{get;} public WaitHandle Handle{get;} public void Start(); public void Dispose(); public void Kill(); public void Join(); public bool Join(int millisecondsTimeout); public bool Join(TimeSpan timeout); public string Name{get;set;} public bool IsAlive{get;} public void Dispose(); }
WorkerThread
provides easy thread-creation and other features, including a Kill()
method for terminating threads (instead of using Abort()
). The potentially dangerous methods of the Thread
class are not present in the interface of WorkerThread
, but the good ones are maintained. WorkerThread
also enforces the best practices of using .NET threads discussed so far. Example 8-15 shows the implementation of WorkerThread
. Because the Thread
class is sealed, I had to use containment rather than derivation when defining WorkerThread
. WorkerThread
has the m_ThreadObj
member variable of type Thread
, representing the underlying wrapped thread. You can access the underlying thread via the Thread
property of WorkerThread
, if you want to be able to do direct thread manipulation.
Example 8-15. The WorkerThread wrapper class
public class WorkerThread : IDisposable { ManualResetEvent m_ThreadHandle; Thread m_ThreadObj; bool m_EndLoop; Mutex m_EndLoopMutex; public override int GetHashCode() { return m_ThreadObj.GetHashCode(); } public override bool Equals(object obj) { return m_ThreadObj.Equals(obj); } public int ManagedThreadId { get { return m_ThreadObj.ManagedThreadId; } } public Thread Thread { get { return m_ThreadObj; } } protected bool EndLoop { set { m_EndLoopMutex.WaitOne(); m_EndLoop = value; m_EndLoopMutex.ReleaseMutex(); } get { bool result = false; m_EndLoopMutex.WaitOne(); result = m_EndLoop; m_EndLoopMutex.ReleaseMutex(); return result; } } public WorkerThread() { m_EndLoop = false; m_ThreadObj = null; m_EndLoopMutex = new Mutex(); m_ThreadHandle = new ManualResetEvent(false); m_ThreadObj = new Thread(Run); Name = "Worker Thread"; } public WorkerThread(bool autoStart) : this() { if(autoStart) { Start(); } } public WaitHandle Handle { get { return m_ThreadHandle; } } public void Start() { Debug.Assert(m_ThreadObj != null); Debug.Assert(m_ThreadObj.IsAlive == false); m_ThreadObj.Start(); } public void Dispose() { Kill(); } void Run() { try { int i = 0; while(EndLoop == false) { Trace.WriteLine("Thread is alive, Counter is " + i); i++; } } finally { m_ThreadHandle.Set(); } } public void Kill() { //Kill() is called on client thread - must use cached Thread object Debug.Assert(m_ThreadObj != null); if(IsAlive == false) { return; } EndLoop = true; //Wait for thread to die Join(); m_EndLoopMutex.Close(); m_ThreadHandle.Close(); } public void Join() { Join(Timeout.Infinite); } public bool Join(int millisecondsTimeout) { TimeSpan timeout = TimeSpan.FromMilliseconds(millisecondsTimeout); return Join(timeout); } public bool Join(TimeSpan timeout) { //Join() is called on client thread - must use cached Thread object Debug.Assert(m_ThreadObj != null); if(IsAlive == false) { return true; } Debug.Assert(Thread.CurrentThread.ManagedThreadId != m_ThreadObj.ManagedThreadId); return m_ThreadObj.Join(timeout); } public string Name { get { return m_ThreadObj.Name; } set { m_ThreadObj.Name = value; } } public bool IsAlive { get { Debug.Assert(m_ThreadObj != null); bool handleSignaled = m_ThreadHandle.WaitOne(0,true); while(handleSignaled == m_ThreadObj.IsAlive) { Thread.Sleep(0); } return m_ThreadObj.IsAlive; } } }
WorkerThread
provides for one-phase thread creation, because it can encapsulate the use of the ThreadStart
delegate. Its constructor accepts a Boolean value called autoStart
. If autoStart
is true
, the constructor will create a new thread and start it:
WorkerThread workerThread;
workerThread = new WorkerThread(true);//Auto-start the worker thread
If autoStart
is false
, or if you’re using the default constructor, you need to call WorkerThread
’s Start()
method, just like when using the raw Thread
class:
WorkerThread workerThread = new WorkerThread(); workerThread.Start();
The thread method of WorkerThread
is the private Run()
method. In Example 8-15, all Run()
is doing is tracing to the Output window the value of a counter. WorkerThread
provides a default name for the underlying thread, but you should provide your own meaningful value for the thread name, using the Name
property. Note that WorkerThread
returns the ID of the underlying thread in its own implementation of ManagedThreadId
.
WorkerThread
provides a Join()
method, which safely asserts that Join()
is called on a different thread (i.e., not the underlying thread) to avoid a deadlock. Join()
also verifies that the thread is alive before it is called on the wrapped thread. One of the shortcomings of the basic Thread
class is that it does not provide a waitable handle of type WaitHandle
for clients to wait for a thread to die. If all you need to wait for is for a single thread to terminate, Join()
is adequate. However, there is no safe way to combine waiting for a thread to terminate with other waiting operations as a single atomic wait request, which creates the potential for a deadlock.
To address this problem WorkerThread
exposes a property called Handle
, of type WaitHandle
, which is signaled when the thread terminates. To implement Handle
, WorkerThread
has a member variable of type ManualResetEvent
, called m_ThreadHandle
. The WorkerThread
constructors instantiate m_ThreadHandle
in a non-signaled state. When the Run()
method returns, it signals the m_ThreadHandle
handle. To ensure that the handle is signaled regardless of how the Run()
method exits, the signaling is done in a finally
statement. WorkerThread
also provides the Boolean property IsAlive
, which not only calls the underlying thread’s IsAlive
property but also verifies that m_ThreadHandle
’s state is consistent (meaning that if the underlying thread is alive the handle is not signaled, and vice versa). You can test whether a ManualResetEvent
object is signaled by waiting on it with a timeout of zero and checking the value retuned by the Wait()
method. There is, however, a potential race condition that IsAlive
needs to cope with: if the Run()
method has signaled the handle but has not yet returned, the underlying thread will still be in the alive state, even though the handle has been signaled. This is possible, of course, only for the briefest of moments. IsAlive
therefore relinquishes control of the reminder of its CPU time quota using Thread.Sleep(0)
, allowing the underlying thread to be switched back in and terminated.
One of the most common synchronization challenges developers face is the task of killing worker threads, usually upon application shutdown. As mentioned previously, you should avoid calling Thread.Abort()
to terminate your threads. Instead, in each iteration, the thread method should check a flag that signals it whether to do another iteration or to return from the method.
As shown in Example 8-15, the thread method Run()
traces to the Output window the value of a counter in a loop:
int i = 0 while(EndLoop == false) { Trace.WriteLine("Thread is alive, Counter is " + i); i++; }
Before every loop iteration, Run()
checks the Boolean property EndLoop
. If EndLoop
is set to false
, Run()
performs another iteration. The Kill()
method provided by WorkerThread
sets EndLoop
to true
, causing Run()
to return and the thread to terminate. EndLoop
actually gets and sets the value of the m_EndLoop
member variable. Because Kill()
is called on a client thread, you must provide thread-safe access to m_EndLoop
. You can use any of the manual locks presented in this chapter: for example, you can lock the whole WorkerThread
object using Monitor
, or you can use ReaderWriterLock
(although it’s excessive for a property that will be written only once). I chose to use Mutex
:
bool EndLoop { set { m_EndLoopMutex.WaitOne(); m_EndLoop = value; m_EndLoopMutex.ReleaseMutex(); } get { bool result = false; m_EndLoopMutex.WaitOne(); result = m_EndLoop; m_EndLoopMutex.ReleaseMutex(); return result; } }
Kill()
should return only when the worker thread is dead. To that end, Kill()
calls Join()
on the worker thread, after verifying the thread is alive. However, because Kill()
is called on the client thread, the WorkerThread
object must store a Thread
object referring to the worker thread as a member variable. Fortunately, there is already such a member—the m_ThreadObj
member variable. You can only store the thread value in the thread method; you can’t store it in the constructor, which executes on the creating client’s thread. This is exactly what Run()
does in this line:
m_ThreadObj = Thread.CurrentThread;
Note that calling Kill()
multiple times is harmless. Also note that Kill()
does the cleanup of closing the mutex and the thread handle. But what if the client never calls Kill()
? To deal with that eventuality, the WorkerThread
class implements IDisposable
and a destructor, both of which call Kill()
:
public void ~WorkerThread() { Kill(); } public void Dispose() { Kill(); }
It’s important to understand that Kill()
isn’t the same as Dispose()
. Kill()
handles execution flow, such as application shutdown or timely termination of threads, whereas Dispose()
caters to memory and resource management and disposes of other resources the WorkerThread
class might hold. Dispose()
only calls Kill()
as a contingency, in case the client developer forgets to do so.