Introduction
I have always disliked AutoResetEvent
and ManuaResetEvent
. They are what Microsoft offers us in place of a while
loop, Thread.Sleep(n)
and some early timeout criteria. But those two classes make your code ugly and complicated, and don't really offer the functionality and clarity that I have always expected.
For instance, the only way to release a waiting thread early is to make sure the worker thread has access to the AutoResetEvent
object, and to call .Set()
. This means that it must either be passed to the running thread, or you must make sure it is in a high enough scope that it is visible to the thread - maybe by making it a private
instance variable.
Also, to evaluate early release of a waiting thread because the worker thread is taking longer than you are willing to allow, you need to start yet ANOTHER thread to monitor the state of the worker thread and pass it the AutoResetEvent
object so that it can call .Set()
and release your waiting thread for you.
It seems to me that this should be trivial by now. Simple.
With WaitTimeout
, we're getting closer.
Using the Code
WaitTimeout
is not a replacement for AutoResetEvent
. It instead provides a different (and clearer) way of visualizing your code flow in certain use cases using a thread synchronization event class.
AutoResetEvent
's Set()
and Reset()
functions don't describe what I'm doing in my application, they describe what's happening internally in that class.
I'm not interested in that.
I'm writing my own application or library, and when I look at my code, I don't want to see .Set()
or Reset()
. I want to know if a thread is waiting or being released. I want to be able to see at a glance how long a thread will wait, and what the early timeout criteria is without having to dig through my code to find the monitor thread I have running to handle that. If I'm waiting on multiple threads to complete, I want to know how many I'm waiting on, and I don't necessarily want to have to create an array of AutoResetEvent
objects to handle it.
It should be simpler, easier and clearer than that.
WaitTimeout
is designed to handle three specific scenarios:
- Simple wait / release situations with a timeout (
AutoResetEvent
like functionality):
private WaitTimeout SimpleWaitTimeout;
private int timeoutDuration;
private void button1_Click(object sender, EventArgs e)
{
try
{
timeoutDuration = int.Parse(tbSimpleWaitSeconds.Text.Trim());
}
catch (Exception)
{
MessageBox.Show("The value you type into the 'Wait Seconds' textbox
must be numerical.", "Typo?", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
yourComplicatedReleaseEarlyCriteria = false;
SimpleWaitTimeout = new WaitTimeout(timeoutDuration);
new Thread(delegate()
{
SimpleWaitTimeout.Wait();
this.Invoke((MethodInvoker)delegate()
{
this.lblStatus.Text = "We're out.";
});
}).Start();
new Thread(delegate()
{
while (!SimpleWaitTimeout.IsTimedOut())
{
this.Invoke((MethodInvoker)delegate()
{
this.lblStatus.Text = "Remaining time: " +
SimpleWaitTimeout.GetRemainingSeconds() + " seconds.";
});
Thread.Sleep(1);
}
}).Start();
}
private void button2_Click(object sender, EventArgs e)
{
SimpleWaitTimeout.Release();
}
- Wait / release scenarios where you want to have early release criteria evaluated without the hassle and clutter, like so:
private void btWaitTimeoutWithDelegate_Click(object sender, EventArgs e)
{
try
{
timeoutDuration = int.Parse(tbWaitSecondsWithDelegate.Text.Trim());
}
catch (Exception)
{
MessageBox.Show("The value you type into the 'Wait Seconds'
textbox must be numerical.", "Typo?", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
yourComplicatedReleaseEarlyCriteria = false;
WaitTimeout waitTimeout = new WaitTimeout(timeoutDuration, delegate()
{ return yourComplicatedReleaseEarlyCriteria; });
new Thread(delegate()
{
waitTimeout.Wait();
this.Invoke((MethodInvoker)delegate()
{
this.lblStatus.Text = "We're out.";
});
}).Start();
new Thread(delegate()
{
while (!waitTimeout.IsTimedOut())
{
this.Invoke((MethodInvoker)delegate()
{
this.lblStatus.Text = "Remaining time: " +
waitTimeout.GetRemainingSeconds() + " seconds.";
});
Thread.Sleep(1);
}
}).Start();
}
private void btWaitTimeoutWithDelegateExitEarly_Click(object sender, EventArgs e)
{
yourComplicatedReleaseEarlyCriteria = true;
}
- And scenarios where you have started multiple worker threads, and you need to wait for them to complete - but you want to stop waiting for them if they take too long, when they all report in, or if some early release criteria is met (for instance, if your end user is tearing his hair out in frustration and mashing the cancel button):
private void btStartMultipleThreadEval_Click(object sender, EventArgs e)
{
try
{
timeoutDuration = int.Parse(tbWaitTimeoutMultipleThreads.Text.Trim());
}
catch (Exception)
{
MessageBox.Show("The value you type into the 'Wait Seconds'
textbox must be numerical.", "Typo?", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
yourComplicatedReleaseEarlyCriteria = false;
WaitTimeout waitTimeout =
new WaitTimeout(timeoutDuration, delegate() { return yourComplicatedReleaseEarlyCriteria; });
bool succeeded = false;
String msg = "";
new Thread(delegate()
{
this.Invoke((MethodInvoker)delegate()
{
this.lblStatus.Text = "Threads completed: 0";
});
for (int threads = 1; threads < 16; threads++)
{
if (!waitTimeout.IsTimedOut())
{
new Thread(delegate(Object o)
{
int thisThreadNumber = (int)o;
new WaitTimeout(500 * thisThreadNumber, delegate()
{ return waitTimeout.IsTimedOut(); }).Wait();
if (!waitTimeout.IsTimedOut())
{
waitTimeout.ReportThreadComplete();
this.Invoke((MethodInvoker)delegate()
{
this.lblStatus.Text = "Threads completed: " +
waitTimeout.GetNumReportedComplete().ToString();
});
}
}).Start(threads);
}
}
succeeded = waitTimeout.WaitOn(15);
msg = "We're out. Threads completed:" +
waitTimeout.GetNumReportedComplete().ToString();
msg += succeeded ? " (Success! All threads complete before timeout.)." : "
(Failure! Timed out or released early before all threads completed.)";
this.Invoke((MethodInvoker)delegate() { this.lblStatus.Text = msg; });
}).Start();
}
private void btMultipleThreadEvalExitEarly_Click(object sender, EventArgs e)
{
yourComplicatedReleaseEarlyCriteria = true;
}
Points of Interest
Yes, I know that Microsoft's TPL and Parallel Foreach can also be used in our "multiple threads reporting" in scenario, but I think if you look at the complication involved, you will agree that this implementation is clearer and simpler. It makes this kind of requirement trivial.
History
- 23rd April, 2016: Initial version