We can leverage the WaitEventHandler and the CountDownEvent as we did with threads to leverage signaling between threads. As a simple demonstration we're going to use the same example we used for locking, we're going to use the pull straw example.
Let's start with the straw enum and a straws class.
enum Straw { Long, Short }
class Straws
{
//used to
randomly select index for short straws
Random rnd = new Random(DateTime.Now.Millisecond);
//array to hold
short straws
Straw[] _straws;
//expose the
number of straws
public int Length { get { return
_straws.Length; } }
//a straw
indexer that lets us extract straws
//and resize
the array removing excess slots
public Straw this[int Index]
{
get {
var pulledStraws = _straws[Index];
for (; Index + 1 < _straws.Length; Index++)
_straws[Index] = _straws[Index
+ 1];
Array.Resize(ref _straws,
_straws.Length - 1);
return pulledStraws;
}
}
//Constructor
to build collection of straws, with defined
//number of
short straws
public Straws(int
NumOfStraws, int
NumOfShortStraws = 1)
{
_straws = new Straw[NumOfStraws];
while (NumOfShortStraws > 0) {
var randomIndex = rnd.Next(_straws.Length);
if (_straws[randomIndex] != Straw.Short) {
_straws[randomIndex] = Straw.Short;
NumOfShortStraws--;
}
}
}
}
The constructor would be a perfect place to do some exception handling, things like
- Passing in negative values
- passing in 0 for number of short straws
- passing in a value for short straws that's greater then the total number of straws
but for brevity I've left such things out.
Next let's take a look at our Person class
class Person
{
//Used to pick
a random straw to select
Random rnd = new Random(DateTime.Now.Millisecond);
//simple
identifier
public string Name { get; set; }
//set to
nullable so that person doesn't start with a straw
public Straw? straw { get; private set; } = null;
//method that
selects a random straw
public void
PullStraw(Straws straws) { straw =
straws[rnd.Next(straws.Length)]; }
//a nice way to
notify who pulled what type of straw
public override string
ToString() { return $"{Name} pulled a {straw} straw"; }
}
pretty straight forward, on the contrived side, but you get the idea.
next let's look at our main, first with the WaitEventHandler usage commented out.
class Program
{
static void Main(string[] args)
{
//define some
people
var ppl = new Person[] { new Person { Name = "Pawel" }, new Person { Name="Magda" },
new Person { Name = "Misha" }, new Person { Name="Jakub" }, new Person { Name = "Tomek" }, new Person { Name = "Marin" } };
//instantiate
our straws to pull
var straws = new Straws(ppl.Length);
//define a
place to hold our tasks
var tasks = new Task[ppl.Length];
//using (var
ewh = new EventWaitHandle(true, EventResetMode.AutoReset))
{
//asyncronously
select straw
for (int i = 0; i
< ppl.Length; i++)
tasks[i] = Task.Factory.StartNew(x =>
{
// ewh.WaitOne();
ppl[int.Parse(x.ToString())].PullStraw(straws);
// ewh.Set();
}, i);
//let all tasks
complete before printing out selections
Task.WaitAll(tasks);
foreach (var p in ppl)
Console.WriteLine(p.ToString());
}
}
}
We crated an array to store all of our tasks as we create them to facilitate waiting for tasks to complete before trying to output our results.
if we run our application as is we'll get some bizarre behavior, from multiple short straws, to no short straws, however if we uncomment our EventWaitHanlde code all starts working as expected.
using (var ewh = new EventWaitHandle(true, EventResetMode.AutoReset))
{
//asyncronously
select straw
for (int i = 0; i
< ppl.Length; i++)
tasks[i] = Task.Factory.StartNew(x =>
{
ewh.WaitOne();
ppl[int.Parse(x.ToString())].PullStraw(straws);
ewh.Set();
}, i);
//let all tasks
complete before printing out selections
Task.WaitAll(tasks);
foreach (var p in ppl)
Console.WriteLine(p.ToString());
}
this works as expected, because of the EventWaitHandle object we created, because we initialize it to true, this means the first time a thread hits the ewh.WaitOne() method call it continues on but blocks all other tasks at that point because we set the Event reset mode to auto, marking the handler as not signaled every time we "Use" a signal; after our person picks their straw we mark the handler as "Signaled" to let another task have a go, and so on. Once all the tasks complete, then our Tasks.WaitAll(tasks) line lets our main thread continue.
so now that we've leveraged EventWaitHandle handler, let's drop this whole keeping track of tasks and waiting for them by leveraging the CountDownEvent.
class Program
{
static void Main(string[] args)
{
//define some
people
var ppl = new Person[] { new Person { Name = "Pawel" },
new Person { Name="Magda" }, new Person { Name = "Misha" },
new Person { Name="Jakub" }, new Person { Name = "Tomek" },
new Person { Name = "Marin" } };
//instantiate
our straws to pull
var straws = new Straws(ppl.Length, 2);
using (var cdh = new CountdownEvent(ppl.Length))
{
using (var ewh = new EventWaitHandle(true, EventResetMode.AutoReset))
{
//asyncronously
select straw
foreach (var person in ppl)
Task.Factory.StartNew(p =>
{
ewh.WaitOne();
((Person)p).PullStraw(straws);
ewh.Set();
cdh.Signal();
}, person);
cdh.Wait();
}
foreach (var p in ppl)
Console.WriteLine(p.ToString());
}
}
}
by using signalling we now have our threads notifying each other of when it's safe to pull a straw and we removed the need to keep track of our tasks by signalling the CountdownEvent of when it's ok to display our results.