Search code examples
c#multithreadingoopautoresetevent

Control threads using AutoResetEvent in C#


Say I have a class A and a class B representing tasks. I want to perform an experiment, and for the experiment to start I need to finish at least 5 B tasks and only 1 A task.

I have the following classes

abstract class Task   
{ 
    public int Id;

    public void Start(object resetEvent)
    {
        EventWaitHandle ewh = (EventWaitHandle)resetEvent;

        Thread.Sleep(new Random(DateTime.Now.Ticks.GetHashCode()).Next(5000, 14000));
        Console.WriteLine("{0} {1} starts",this.GetType().Name, Id);
        ewh.Set();
    }
}

class A : Task
{
    static int ID = 1;
    public A(EventWaitHandle resetEvent)
    {
        Id = ID++;
        new Thread(StartTask).Start(resetEvent);
    }
}

class B : Task
{
    static int ID = 1;
    public B(EventWaitHandle resetEvent)
    {
        Id = ID++;
        new Thread(StartTask).Start(resetEvent);
    }
}

and the following main

static void Main()
{
    A a;

    B[] bs = new B[20];
    int numberOfBs = 0;

    EventWaitHandle aResetEvent = new AutoResetEvent(false);
    EventWaitHandle bResetEvent = new AutoResetEvent(false);

    a = new A(aResetEvent);
    for (int i = 0; i < bs.Length; i++)
        bs[i] = new B(bResetEvent);

    while (numberOfBs < 5)
    {
        bResetEvent.WaitOne();
        numberOfBs++;
    }
    aResetEvent.WaitOne();

    Console.WriteLine("Experiment started with {0} B's!", numberOfBs);
    Thread.Sleep(3000); // check how many B's got in the middle
    Console.WriteLine("Experiment ended with {0} B's!", numberOfBs);
}

now I have few problems/questions:

  1. How can I wait for only N signals out of possible M?

  2. Can I achieve the result I'm looking for with only 1 AutoResetEvent?

  3. I don't understand why all the tasks are printed together, I expected each task to be printed when it is done and now when everything is done.

  4. is the following code thread safe?

.

while (numberOfBs < 5)
{
    bResetEvent.WaitOne();
    numberOfBs++;
}

could it be that couple of threads signal together? if so, can I fix that using lock on bResetEvent?


Solution

  • 1.How can I wait for only N signals out of possible M?

    Just as you do here (sort of…see answer to #4).

    2.Can I achieve the result I'm looking for with only 1 AutoResetEvent?

    Yes. But you will need two counters in that case (one for the A type and one for the B type), and they will need to be accessed in a thread-safe way, e.g. with the Interlocked class, or using a lock statement. All threads, A and B types, will share the same AutoResetEvent, but increment their own type's counter. The main thread can monitor each counter and process once both counters are at their desired value (1 for the A counter, 5 for the B counter).

    I'd recommend using the lock statement approach, as it's simpler and would allow you to avoid using AutoResetEvent altogether (the lock statement uses the Monitor class, which provides some functionality similar to AutoResetEvent, while also providing the synchronization needed to ensure coherent use of the counters.

    Except that you've written in the comments you have to use AutoResetEvent (why?), so I guess you're stuck with Interlocked (no point in using lock if you're not going to take full advantage).

    3.I don't understand why all the tasks are printed together, I expected each task to be printed when it is done and now when everything is done.

    Because you have a bug. You should be creating a single Random instance and using it to determine the duration of every task. You can either compute the durations in the thread that creates each task, or you can synchronize access (e.g. with lock) and use the same Random object in multiple threads.

    What you can't do is create a whole new Random object using the same seed value for every thread, because then each thread (or at least large blocks of them, depending on timing) is going to wind up getting the exact same "random" number to use as its duration.

    You see all the output coming out together, because that's when it happens: all together.

    (And yes, if you create multiple Random objects in quick succession, they will all get the same seed, whether you use DateTime.Now yourself explicitly, or just let the Random class do it. The tick counter used for the seed is not updated frequently enough for concurrently running threads to see different values.)

    4.is the following code thread safe?

    The code in question:

    while (numberOfBs < 5)
    {
        bResetEvent.WaitOne();
        numberOfBs++;
    }
    

    …is thread safe, because the only data shared between the thread executing that loop and any other thread is the AutoResetEvent object, and that object is itself thread-safe.

    That is, for the usual understanding of "thread safe". I highly recommend you read Eric Lippert's article What is this thing you call "thread safe"? Asking if something is thread-safe is a much more complicated question that you probably realize.

    In particular, while the code is thread-safe in the usual way (i.e. data remains coherent), as you note it is possible for more than one thread to reach the Set() call before the main thread can react to the first. Thus you may miss some notifications.