Section 8.7. The WorkerThread Wrapper Class


8.7. The WorkerThread Wrapper Class

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;       }    } }

8.7.1. Launching a New Worker Thread

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.

8.7.2. Joining WorkerThread and the Thread Handle

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.

8.7.3. Terminating the Worker Thread

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 memberthe 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.



Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net