Wednesday, 26 April 2017

TPL 06 Signalling

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.