Specifically, I'm wondering:
Will the ManualResetEvent consume resources while it is in a wait state? Does the performance degradation of context switching apply to threads that are in a wait state?
If I have a choice to use multiple BackgroundThreadQueues that do less work each, or one BackgroundThreadQueue that does more work, and I choose to use multiple...will the waiting thread queues affect process performance while they are not doing anything?
Is there a better FIFO thread queue I should be using in C#, or a different locking strategy?
Any suggestions are appreciated.
/// <summary>
/// This class is responsible for peforming actions in a FIFO order on a
/// background thread. When it is constructed, a background thread is created
/// and a manual reset event is used to trigger actions to be performed when
/// a new action is enqueued, or when one finishes. There is a ShuttingDown
/// flag that is set by calling code when it is time to destroy the thread,
/// and a QueueIsEmpty event is fired whenever the queue finishes executing
/// the last action.
/// </summary>
public class BackgroundThreadQueue : IBackgroundThreadQueue
{
#region Fields
private readonly Queue<Action> queueOfActions = new Queue<Action>();
readonly ManualResetEvent resetEvent;
private bool shuttingDown;
private bool readyToShutdown;
private readonly object lockObject = new object();
private string queuName;
#endregion Fields
#region Events
/// <summary>
/// Occurs when the BackgroundThreadQueue is empty, and ready to shut down.
/// </summary>
public event EventHandler IsReadyToShutdown;
#endregion Events
#region Constructor
public BackgroundThreadQueue(string threadName)
{
this.resetEvent = new ManualResetEvent(false);
queuName = threadName;
StartThread();
}
#endregion Constructor
#region Public Methods
public void ClearQueue()
{
lock (lockObject)
{
queueOfActions.Clear();
}
resetEvent.Set();
}
/// <summary>
/// Enqueues an action, and calls set on the manual reset event to trigger
/// the action to be performed (if no action is currently being performed,
/// the one just enqueued will be done immediately, if an action is already
/// being performed, then the one just enqueued will have to wait its turn).
/// </summary>
public void EnqueueAction(Action actionToEnqueue)
{
if (actionToEnqueue == null)
{
throw new ArgumentNullException("actionToEnqueue");
}
bool localReadyToShutDown = false;
lock (lockObject)
{
queueOfActions.Enqueue(actionToEnqueue);
if(this.readyToShutdown)
{
localReadyToShutDown = true;
this.readyToShutdown = false;
}
}
//if this instance is ready to shut down...and we just enqueued a
//new action...we can't shut down now...
if (localReadyToShutDown)
{
StartThread();
}
resetEvent.Set();
}
#endregion Public Methods
#region Public Properties
public bool ReadyToShutdown
{
get
{
lock (lockObject)
{
return this.shuttingDown && this.readyToShutdown;
}
}
private set
{
this.readyToShutdown = value;
if (this.readyToShutdown)
{
//let interested parties know that the queue is now empty
//and ready to shutdown
IsReadyToShutdown.Raise(this);
}
}
}
/// <summary>
/// Gets or sets a value indicating whether or not the queue should shut down
/// when it is finished with the last action it has enqueued to process.
/// If the queues owner is shutting down, it needs to notify the queue,
/// and wait for a QueueIsEmpty event to be fired, at which point the reset
/// event will exit ... the owner shouldn't actually destroy the queue
/// until all actions have been performed.
/// </summary>
public bool ShuttingDown
{
get
{
lock (lockObject)
{
return this.shuttingDown;
}
}
set
{
lock (lockObject)
{
bool startThread = false;
if (value == false)
{
readyToShutdown = false;
//if we were shutting down...but, now are not
startThread = this.shuttingDown;
}
this.shuttingDown = value;
//if we were shutting down, but now are not...
//we need to restart the processing actions thread
if (startThread)
{
StartThread();
}
}
this.resetEvent.Set();
}
}
#endregion Public Properties
#region Private Methods
private void StartThread()
{
var processActionsThread = new Thread(this.ProcessActions);
processActionsThread.Name = queuName;
processActionsThread.IsBackground = true;
processActionsThread.Start();
}
/// <summary>
/// Processes the actions in a while loop, resetting a ManualResetEvent that
/// is triggered in the EnqueueAction method and ShuttingDown property.
/// </summary>
private void ProcessActions()
{
while (true)
{
Action action = null;
lock (lockObject)
{
//if there are any actions, then get the first one out of the queue
if (queueOfActions.Count > 0)
{
action = queueOfActions.Dequeue();
}
}
if (action != null)
{
action();
}
lock (lockObject)
{
//if any actions were added since the last one was processed, go
//back around the loop and do the next one
if (this.queueOfActions.Count > 0)
{
continue;
}
if (this.shuttingDown)
{
//ReadyToShutdown setter will raise IsReadyToShutdown
ReadyToShutdown = true;
//get out of the method if the user has chosen to shutdown,
//and there are no more actions to process
return;
}
this.resetEvent.Reset();
}
this.resetEvent.WaitOne();
}
}
#endregion Private Methods
}