Friday, January 8, 2010

Revised WorkerThread class and Bug in EventWaitHandle.WaitOne() in .Net CF 2.0

The WaitOne(int millisecondsTimeout, bool exitContext) method of the ManualResetEvent (based on EventWaitHandle) class allows waiting for a synchronization flag to be set, but timing out if it does not. On the .Net Compact Framework (.NetCF) 2.0, calling this method with exitContext set to true throws an ArgumentException exception. The fix is to always set exitContext to false.

How I got into this was researching .Net worker threads. There was a very good article written by Juval Löwy, titled Working with .NET Threads, and provided a useful wrapper class, named WorkerThread, to make managing threads easier in your application. Thank you, Juval!

I use .NetCF frequently and, as usual, found some incompatibilities I had to correct before the WorkerThread class worked on that platform. The revised and better-documented WorkerThread for .NetCF 2.0 is listed below. Even after rewriting for the methods missing in the compact framework, I ran across a bug in .NetCF 2.0 that prevents the class from working properly.

The wrapper class relies on several synchronization constructs in order to provide safe access to its functionality across threads. One such case is where an extra integrity check attempts to determine that the thread is actually running, in the code that implements the IsAlive property. As originally coded, it was:


public bool IsAlive
{
get
{
Debug.Assert(m_ThreadObj != null);
bool isAlive = m_ThreadObj.IsAlive;
bool handleSignaled = m_ThreadHandle.WaitOne(0,true);
Debug.Assert(handleSignaled == ! isAlive);
return isAlive;
}
}


The problem appears in the call to WaitOne(0,true). While it compiles fine, at runtime it throws an ArgumentException error instead of ignoring or saving & restoring the synchronization context (a completely different concept than the thread synchronization I'm writing about here). Simple to fix, but maddening to diagnose.

The other minor issues I fixed were:

  • No IsAlive property on the .NetCF Thread class. Reference was commented out. I added a new instance variable, m_IsAlive, to emulate the missing property.

  • No Join(TimeSpan timeout) method on the .NetCF Thread class. Translated that method to use Join(int millisecondsTimeout) instead.

  • Fixed IsAlive property code to avoid the ArgumentException error, as mentioned above.

  • Added a Yield() method to relinquish the processor to other threads as an alternative to using the more cryptic Thread.Sleep(1).

  • Added an overridable Work() method to allow this class to be inherited instead of copied and modified. Replace Work() in your derived class to implement your processing.

  • Modified the Run() method to call Work() and automatically manage the IsAlive property.



As a way to pass on the bits of knowledge I've gained from this exercise, here is the final source for the .NetCF 2.0 WorkerThread class:


////////////////////////////////////////////////////////////////////////////////
// File.......: WorkerThread.cs
// Author.....: Edward F Eaglehouse
// Date.......: 11/21/2009
// Notes......:
// Adapted for .NetCF 2.0 from the WorkerThread class by Juval Lowy in his
// article, Working with .NET Threads. The full article text can be found at
// http://www.devx.com/codemag/Article/17442/0/page/1.
////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;

namespace gltech.trackaway
{
///
/// Class that implements a separate worker thread.
///

///
/// Based closely on the WorkerThread wrapper class described in the
/// article Working with .NET Threads,
/// by Juval Löwy. Modified so it works with the .NET Compact Framework.
///

public class WorkerThread : IDisposable
{

#region Protected Members

///
/// Synchronization construct for signalling completion of the thread.
///

protected ManualResetEvent m_ThreadHandle;

///
/// Contains a reference to the uninheritable underlying Thread object.
///

protected Thread m_ThreadObj;

///
/// Flag to indicate that thread processing should be stopped.
///

protected bool m_EndLoop;

///
/// Synchronization construct to protect the EndLoop flag.
///

protected Mutex m_EndLoopMutex;

///
/// Flag to indicate the thread is actively running.
///

protected bool m_IsAlive;

#endregion

#region Constructors

///
/// Initialize a new instance of the WorkerThread class.
///

public WorkerThread()
{
m_EndLoop = false;
m_ThreadObj = null;
m_EndLoopMutex = new Mutex();
m_ThreadHandle = new ManualResetEvent(false);

ThreadStart threadStart = new ThreadStart(Run);
m_ThreadObj = new Thread(threadStart);
m_ThreadObj.Name = "Worker Thread";
m_IsAlive = false;
}

///
/// Initialize a new instance of the WorkerThread class.
///

/// True to launch the thread immediately; otherwise, false.
public WorkerThread(bool autoStart)
: this()
{
if (autoStart)
{
Start();
}
}

#endregion

#region Properties

///
/// Get the underlying Thread object.
///

public Thread Thread
{
get
{
return m_ThreadObj;
}
}

///
/// Get the thread completion synchronization object.
///

public WaitHandle Handle
{
get
{
return m_ThreadHandle;
}
}

///
/// Get the indicator that the thread is active.
///

public bool IsAlive
{
get
{
Debug.Assert(m_ThreadObj != null);
//bool isAlive = m_Thread.IsAlive;
bool isAlive = this.m_IsAlive;
//bool handleSignaled = m_ThreadHandle.WaitOne(0, true);
bool handleSignaled = m_ThreadHandle.WaitOne(0, false);
Debug.Assert(handleSignaled == !isAlive);
return isAlive;
}
}

///
/// Get or set the indicator that processing should stop.
///

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

///
/// Get or set the debuggable thread name.
///

public string Name
{
get
{
return m_ThreadObj.Name;
}
set
{
m_ThreadObj.Name = value;
}
}

#endregion

#region Public Methods

///
/// Launch the thread.
///

public void Start()
{
Debug.Assert(m_ThreadObj != null);
//Debug.Assert(m_ThreadObj.IsAlive == false);
m_ThreadObj.Start();
} // Start()

///
/// Overridable method that implements the processing to be done.
///

public virtual void Work()
{
int i = 0;
while (EndLoop == false)
{
Debug.WriteLine("Thread is alive, Counter is " + i);
i++;
}
} // Work()

///
/// Destroy this WorkerThread instance.
///

public void Dispose()
{
Kill();
} // Dispose()

///
/// Notify the processing loop to stop.
///

public void Kill()
{
//Kill is called on client thread - must use cached object
Debug.Assert(m_ThreadObj != null);
if (IsAlive == false)
{
return;
}
EndLoop = true;
//Wait for thread to die
Join();
m_EndLoopMutex.Close();
m_ThreadHandle.Close();
} // Kill()

///
/// Blocks the calling thread until this thread terminates.
///

public void Join()
{
Debug.Assert(m_ThreadObj != null);
if (IsAlive == false)
{
return;
}
Debug.Assert(Thread.CurrentThread.GetHashCode() !=
m_ThreadObj.GetHashCode());
m_ThreadObj.Join();
} // Join()

///
/// Blocks the calling thread until this thread terminates or the
/// specified time elapses.
///

/// Number of milliseconds to wait.
///
public bool Join(int millisecondsTimeout)
{
TimeSpan timeout;
timeout = TimeSpan.FromMilliseconds(millisecondsTimeout);
return Join(timeout);
} // Join(int millisecondsTimeout)

///
/// Blocks the calling thread until this thread terminates or the
/// specified time elapses.
///

/// TimeSpan set to the amount of time to wait.
///
public bool Join(TimeSpan timeout)
{
int timeout_ms = (int)timeout.TotalMilliseconds;

Debug.Assert(m_ThreadObj != null);
if (IsAlive == false)
{
return true;
}
Debug.Assert(Thread.CurrentThread.GetHashCode() !=
m_ThreadObj.GetHashCode());
return m_ThreadObj.Join(timeout_ms);
} // Join(TimeSpan timeout)

///
/// Suspend this thread to allow other waiting threads to execute.
///

public void Yield()
{
// Yield processor to other threads, even ones at a lower priority.
Thread.Sleep(1);
} // Yield()

#endregion

#region Protected Methods

///
/// Do the processing defined by this thread.
///

protected void Run()
{
try
{
m_IsAlive = true;
Work();
}
finally
{
m_IsAlive = false;
m_ThreadHandle.Set();
}
} // Run()

#endregion

}
}

No comments:

Post a Comment