21

In my quest to build a condition variable class I stumbled on a trivially simple way of doing it and I'd like to share this with the stack overflow community. I was googling for the better part of an hour and was unable to actually find a good tutorial or .NET-ish example that felt right, hopefully this can be of use to other people out there.

John Leidegren
  • 59,920
  • 20
  • 131
  • 152

6 Answers6

30

It's actually incredibly simple, once you know about the semantics of lock and Monitor.

But first, you do need an object reference. You can use this, but remember that this is public, in the sense that anyone with a reference to your class can lock on that reference. If you are uncomfortable with this, you can create a new private reference, like this:

readonly object syncPrimitive = new object(); // this is legal

Somewhere in your code where you'd like to be able to provide notifications, it can be accomplished like this:

void Notify()
{
    lock (syncPrimitive)
    {
        Monitor.Pulse(syncPrimitive);
    }
}

And the place where you'd do the actual work is a simple looping construct, like this:

void RunLoop()
{
    lock (syncPrimitive)
    {
        for (;;)
        {
            // do work here...
            Monitor.Wait(syncPrimitive);
        }
    }
}

Yes, this looks incredibly deadlock-ish, but the locking protocol for Monitor is such that it will release the lock during the Monitor.Wait. In fact, it's a requirement that you have obtained the lock before you call either Monitor.Pulse, Monitor.PulseAll or Monitor.Wait.

There's one caveat with this approach that you should know about. Since the lock is required to be held before calling the communication methods of Monitor you should really only hang on to the lock for an as short duration as possible. A variation of the RunLoop that's more friendly towards long running background tasks would look like this:

void RunLoop()
{
    
    for (;;)
    {
        // do work here...

        lock (syncPrimitive)
        {
            Monitor.Wait(syncPrimitive);
        }
    }
}

But now we've changed up the problem a bit, because the lock is no longer protecting the shared resource throughout the processing. So, if some of your code in the do work here... bit needs to access a shared resource you'll need an separate lock managing access to that.

We can leverage the above to create a simple thread-safe producer consumer collection (although .NET already provides an excellent ConcurrentQueue<T> implementation; this is just to illustrate the simplicity of using Monitor in implementing such mechanisms).

class BlockingQueue<T>
{
    // We base our queue on the (non-thread safe) .NET 2.0 Queue collection
    readonly Queue<T> q = new Queue<T>();

    public void Enqueue(T item)
    {
        lock (q)
        {
            q.Enqueue(item);
            System.Threading.Monitor.Pulse(q);
        }
    }

    public T Dequeue()
    {
        lock (q)
        {
            for (;;)
            {
                if (q.Count > 0)
                {
                    return q.Dequeue();
                }
                System.Threading.Monitor.Wait(q);
            }
        }
    }
}

Now the point here is not to build a blocking collection, that also available in the .NET framework (see BlockingCollection). The point is to illustrate how simple it is to build an event driven message system using the Monitor class in .NET to implement conditional variable. Hope you find this useful.

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
John Leidegren
  • 59,920
  • 20
  • 131
  • 152
  • Do I really need to `lock(syncPrimitive)` **before** `Monitor.Pulse(syncPrimitive)`? I understand that I need to `lock(q)` before `Monitor.Pulse(q)` because I'm reading/modifying `q`, but I dont understand the need to `lock` in the previous case. – Nawaz Feb 03 '16 at 12:58
  • 2
    @Nawaz That something that has been debated in the other answer to this question. If we take a look at the source code there's a note about it but it doesn't say why http://referencesource.microsoft.com/#mscorlib/system/threading/monitor.cs,12029e7542a2ec9c,references it only says that it will throw a SynchronizationLockException if you call `Pulse` outside of a block of synchronized code. – John Leidegren Feb 04 '16 at 12:28
  • 2
    @Nawaz the documentation for the `SynchronizationLockException` says that it's supposed to be thrown when you don't own the lock and the operation expects you to. So, it does give some indication to why this is the case. Whether it is a limitation in the framework or technical detail I cannot say but it's definitely required. – John Leidegren Feb 04 '16 at 12:30
  • Yes. I read the documentation after commenting here. Now it is clear to me that the lock is necessary. Thanks for confirming it. – Nawaz Feb 04 '16 at 13:15
9

Use ManualResetEvent

The class that is similar to conditional variable is the ManualResetEvent, just that the method name is slightly different.

The notify_one() in C++ would be named Set() in C#.
The wait() in C++ would be named WaitOne() in C#.

Moreover, ManualResetEvent also provides a Reset() method to set the state of the event to non-signaled.

Wong Jia Hau
  • 2,639
  • 2
  • 18
  • 30
5

The accepted answer is not a good one. According to the Dequeue() code, Wait() gets called in each loop, which causes unnecessary waiting thus excessive context switches. The correct paradigm should be, wait() is called when the waiting condition is met. In this case, the waiting condition is q.Count() == 0.

Here's a better pattern to follow when it comes to using a Monitor. https://msdn.microsoft.com/en-us/library/windows/desktop/ms682052%28v=vs.85%29.aspx

Another comment on C# Monitor is, it does not make use of a condition variable(which will essentially wake up all threads waiting for that lock, regardless of the conditions in which they went to wait; consequently, some threads may grab the lock and immediately return to sleep when they find the waiting condition hasn't been changed). It does not provide you with as find-grained threading control as pthreads. But it's .Net anyway, so not completely unexpected.

=============upon the request of John, here's an improved version=============

class BlockingQueue<T>
{
    readonly Queue<T> q = new Queue<T>();

    public void Enqueue(T item)
    {
        lock (q)
        {
            while (false) // condition predicate(s) for producer; can be omitted in this particular case
            {
                System.Threading.Monitor.Wait(q);
            }
            // critical section
            q.Enqueue(item);
        }

        // generally better to signal outside the lock scope
        System.Threading.Monitor.Pulse(q);
    }

    public T Dequeue()
    {
        T t;
        lock (q)
        {
            while (q.Count == 0) // condition predicate(s) for consumer
            {
                System.Threading.Monitor.Wait(q);
            }

            // critical section
            t = q.Dequeue();
        }

        // this can be omitted in this particular case; but not if there's waiting condition for the producer as the producer needs to be woken up; and here's the problem caused by missing condition variable by C# monitor: all threads stay on the same waiting queue of the shared resource/lock.
        System.Threading.Monitor.Pulse(q);

        return t;
    }
}

A few things I'd like to point out:

1, I think my solution captures the requirements & definitions more precisely than yours. Specifically, the consumer should be forced to wait if and only if there's nothing left in the queue; otherwise it's up to the OS/.Net runtime to schedule threads. In your solution, however, the consumer is forced to wait in each loop, regardless whether it has actually consumed anything or not - this is the excessive waiting/context switches I was talking about.

2, My solution is symmetric in the sense that both the consumer and the producer code share the same pattern while yours is not. If you did know the pattern and just omitted for this particular case, then I take back this point.

3, Your solution signals inside the lock scope, while my solutions signals outside the lock scope. Please refer to this answer as to why your solution is worse. why should we signal outside the lock scope

I was talking about the flaw of missing condition variables in C# monitor, and here's its impact: there's simply no way for C# to implemented the solution of moving the waiting thread from the condition queue to the lock queue. Therefore, the excessive context switch is doomed to take place in the three-thread scenario proposed by the answer in the link.

Also, the lack of condition variable makes it impossible to distinguish between the various cases where threads wait on the same shared resource/lock, but for different reasons. All waiting threads are place on a big waiting queue for that shared resource, which undermines efficiency.

"But it's .Net anyway, so not completely unexpected" --- it's understandable that .Net does not pursue as high efficiency as C++, it's understandable. But it does not imply programmers should not know the differences and their impacts.

Community
  • 1
  • 1
h9uest
  • 10,958
  • 3
  • 18
  • 24
  • I fail to see how the waiting condition isn't met. As long as the queue is non-empty (at the point at which it has acquired the lock), it will not enter a wait state. Besides, it's unlikely that context switching is the source of your performance problem (if you have one). – John Leidegren Jan 26 '15 at 17:27
  • A practical example: what if the queue is bounded by certain size? The producer cannot enqueue when the queue is full; The consumer cannot dequeue when the queue is empty. To start with an empty queue, consumers should not be woken up at all until the waiting condition has changed(i.e. queue.Count !=0). In my example, I just have one extra waiting condition; in a real life example, there can be many. Having n condition variables can generally reduce the threads to be woken up by a factor of n. – h9uest Jan 26 '15 at 17:35
  • It's just a silly example of how to do producer/consumer easily on top a non thread safe collection. If you have a *better* solution in mind just show me the code. – John Leidegren Jan 26 '15 at 19:26
  • @JohnLeidegren I've included the full pattern for programming with a monitor. Let me know what you think. – h9uest Jan 26 '15 at 21:20
  • I think you raise several good points here, although it wasn't all that clear initially. Efficiencies aside, I think you need to take a closer look at the actual implementation to be able to rule out whether the arguments about conditional variable efficiencies are valid or not. The .NET CLR is still closed source but the mono project has an open source Monitor implementation (https://github.com/mono/mono/blob/18751d4475cda0456b25597c3d4f9634c14f8a68/mono/metadata/monitor.c) at least it's a start. It's certainly not a limitation of the underlying operating system. – John Leidegren Jan 27 '15 at 08:11
  • No there's no need to check the implementation. The lack of condition variable in the interface(e.g. check out the function signature for Monitor.Wait) makes it impossible for anyone to implement the optimal solution, which involves the condition queue as well as the lock queue. Simple as that. – h9uest Jan 27 '15 at 09:47
  • If you don't agree, feel free to enlighten me how you would implement the solution without condition variables. – h9uest Jan 27 '15 at 09:48
  • There are various way you can achieve the same result but as to whether they are as efficient or not I cannot say without benchmarking something in the real world. What function (or aspect) of the pthread API would be sufficient in your case (or what would you do differently with a pthread implementation)? – John Leidegren Jan 27 '15 at 10:25
  • @h9uest: "my solutions signals outside the lock scope." I have tried this and it throws an exception; `Monitor.Pulse()` must execute within lock scope. https://msdn.microsoft.com/en-us/library/system.threading.monitor.pulse%28v=vs.110%29.aspx – Mike C Jul 03 '15 at 00:48
  • @MikeC Thanks for posting this link. I would also stick the MS solution. posix isn't the only platform c# runs on, and just because it's better to signal outside the lock scope doesn't mean that it is also better (or even works) on other platforms. Imho at least not until the c# documentation says so :) – kritzikratzi Jul 14 '22 at 07:44
  • @kritzikratzi Win32 has condition variable equivalent since years ago (WaitOnAddress, sort of more similar to futex). No idea why MS do not expose it in .NET. – user2771324 Sep 26 '22 at 11:43
4

Go to deadlockempire.github.io/. They have an amazing tutorial that will help you understand the condition variable as well as locks and will cetainly help you write your desired class.

You can step through the following code at deadlockempire.github.io and trace it. Here is the code snippet

while (true) {
  Monitor.Enter(mutex);
  if (queue.Count == 0) {
    Monitor.Wait(mutex);
  }
  queue.Dequeue();
  Monitor.Exit(mutex);
}

while (true) {
  Monitor.Enter(mutex);
  if (queue.Count == 0) {
    Monitor.Wait(mutex);
  }
  queue.Dequeue();
  Monitor.Exit(mutex);
}

while (true) {
  Monitor.Enter(mutex);
  queue.Enqueue(42);
  Monitor.PulseAll(mutex);
  Monitor.Exit(mutex);
}
ShowLove
  • 899
  • 1
  • 12
  • 21
  • 1
    Just clarifying, deadlockempire.github.io is a site full of fun challenges for readers to understand how NOT to code. The goal of the challenges are to step through the code and cause an exception or a deadlock or a race condition. The example here will throw an InvalidOperationException for trying to read from an already empty queue. The way this happens is Thread 0 and 1 both run to the Wait(). Then Thread 2 executes the pulse waking thread 0 and 1 and then exiting. Then Thread 0 runs to exit and then Thread 1 can runs to exit. However 2 deques will happen. – Henri Jun 11 '20 at 20:43
1

As has been pointed out by h9uest's answer and comments the Monitor's Wait interface does not allow for proper condition variables (i.e. it does not allow for waiting on multiple conditions per shared lock).

The good news is that the other synchronization primitives (e.g. SemaphoreSlim, lock keyword, Monitor.Enter/Exit) in .NET can be used to implement a proper condition variable.

The following ConditionVariable class will allow you to wait on multiple conditions using a shared lock.

class ConditionVariable
{
  private int waiters = 0;
  private object waitersLock = new object();
  private SemaphoreSlim sema = new SemaphoreSlim(0, Int32.MaxValue); 

  public ConditionVariable() { 
  }

  public void Pulse() {

      bool release;

      lock (waitersLock)
      {
         release = waiters > 0;
      }

      if (release) {
        sema.Release();
      }
  }

  public void Wait(object cs) {

    lock (waitersLock) {
      ++waiters;
    }

    Monitor.Exit(cs);

    sema.Wait();

    lock (waitersLock) {
      --waiters;
    }

    Monitor.Enter(cs);
  }
}

All you need to do is create an instance of the ConditionVariable class for each condition you want to be able to wait on.

object queueLock = new object();

private ConditionVariable notFullCondition = new ConditionVariable();
private ConditionVariable notEmptyCondition = new ConditionVariable();

And then just like in the Monitor class, the ConditionVariable's Pulse and Wait methods must be invoked from within a synchronized block of code.

T Take() {

  lock(queueLock) {

    while(queue.Count == 0) {

      // wait for queue to be not empty
      notEmptyCondition.Wait(queueLock);
    }

    T item = queue.Dequeue();

    if(queue.Count < 100) {

      // notify producer queue not full anymore
      notFullCondition.Pulse();
    }

    return item;
  }
}

void Add(T item) {

  lock(queueLock) {

    while(queue.Count >= 100) {

      // wait for queue to be not full
      notFullCondition.Wait(queueLock);
    }

    queue.Enqueue(item);

    // notify consumer queue not empty anymore
    notEmptyCondition.Pulse();
  }
}

Below is a link to the full source code of a proper Condition Variable class using 100% managed code in C#.

https://github.com/CodeExMachina/ConditionVariable

gm127
  • 49
  • 4
  • Neat! Looks very clean, I have to try this out. – John Leidegren Aug 30 '17 at 14:32
  • You should use a `while (Monitor.IsEntered(cs)) { }` loop to exit the lock as many times as it was entered, store that count, then re-enter the lock that many times, to work with recursive locks. It would also help to use that to verify the lock is entered at the call site. – Tim Mar 09 '23 at 15:43
0

i think i found "The WAY" on the tipical problem of a

List<string> log; 

used by multiple thread, one tha fill it and the other processing and the other one empting

avoiding empty

    while(true){
    //stuff
    Thread.Sleep(100)
    }

variables used in Program

    public static readonly List<string> logList = new List<string>();

    public static EventWaitHandle evtLogListFilled = new AutoResetEvent(false);

the processor work like

private void bw_DoWorkLog(object sender, DoWorkEventArgs e)
    {
        StringBuilder toFile = new StringBuilder();
        while (true)
        {
            try
            {
                {
                    //waiting form a signal
                    Program.evtLogListFilled.WaitOne();
                    try
                    {
                        //critical section
                        Monitor.Enter(Program.logList);
                        int max = Program.logList.Count;
                        for (int i = 0; i < max; i++)
                        {
                            SetText(Program.logList[0]);
                            toFile.Append(Program.logList[0]);
                            toFile.Append("\r\n");
                            Program.logList.RemoveAt(0);
                        }
                    }
                    finally
                    {
                        Monitor.Exit(Program.logList);
                        // end critical section
                    }


                    try
                    {
                        if (toFile.Length > 0)
                        {
                            Logger.Log(toFile.ToString().Substring(0, toFile.Length - 2));
                            toFile.Clear();
                        }
                    }
                    catch
                    {

                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), ex);
            }
            Thread.Sleep(100);

        }
    }

On the filler thread we have

public static void logList_add(string str)
    {
        try
        {
            try
            {
                //critical section
                Monitor.Enter(Program.logList);
                Program.logList.Add(str);
            }
            finally
            {
                Monitor.Exit(Program.logList);
                //end critical section
            }
            //set start
            Program.evtLogListFilled.Set();

        }
        catch{}

    }

this solution is fully tested, the istruction Program.evtLogListFilled.Set(); may release the lock on Program.evtLogListFilled.WaitOne() and also the next future lock.

I think this is the simpliest way.

  • 1
    I think a BlockingCollection is much simpler, have you tested your solution against that? It's quite efficient. – John Leidegren Aug 30 '17 at 14:30
  • Yeah, there are a lot of things in there that you don't really care about. Historically but to a lesser degree today Microsoft does make things unnecessarily complex and then seldom get back to things that have been released to fix them for the same reason, it's an unfortunate complex mess more often than not. – John Leidegren Sep 21 '17 at 07:20