views:

164

answers:

2

Monitor.Pulse and PulseAll requires that the lock it operates on is locked at the time of call. This requirement seems unnecessary and detrimental for performance. My first idea was that this results in 2 wasted context switches, but this was corrected by nobugz below (thanks). I am still unsure whether it involves a potential for wasted context switches, as the other thread(s) which were waiting on the monitor are already available for the sheduler, but if they are scheduled, they will only be able to run a few instructions before hitting the mutex, and having to context-switch again. This would look much simpler and faster if the lock was unlocked before invoking the Monitor.Pulse.

Pthread condition variables implement the same concept, but it does not have the above described limitation: you can call pthread_cond_broadcast even if you do not own the mutex. I see this as a proof that the requirement is not justified.

Edit: I realize that a lock is required to protect the shared resource that is usually changed before the Monitor.Pulse. I was trying to say that that lock could have been unlocked after access to the resource but before the Pulse, given that Monitor would support this. This would help in limiting the lock to the shortest time during which the shared resource is accessed. As such:

void f(Item i)
{
  lock(somequeue) {
    somequeue.add(i);
  }
  Monitor.Pulse(somequeue);  // error
}
+2  A: 

The reason has to do with memory barriers and guaranteeing thread safety.

Shared variables (conditionals) that are used to determine whether a Pulse() is needed will be checked by all threads involved. Without a memory barrier, the changes might be kept in a register and be invisible from one thread to another. Reads and writes can also be re-ordered when viewed across threads.

However, variables that are accessed from within a lock use a memory barrier, so they are accessible to all related threads. All operations within the lock appear to execute atomically from the perspective of other threads holding the same lock.

Also, multiple context switches aren't required, as you postulated. Waiting threads are put in a (nominally FIFO) queue, and while they're triggered with Pulse(), they aren't fully runnable until the lock is relinquished (again, in part due to memory barriers).

For a good discussion of the issues, see: http://www.albahari.com/threading/part4.aspx#_Wait_and_Pulse

RickNZ
Putting a memory barrier in Pulse() wouldn't address the variables that are checked to see if Pulse() should be called. You know: `lock (someLock) { if (someVariable) Monitor.Pulse(); someVariable = false; }` It is actually possible to build a lightweight signaling mechanism with memory barriers alone... A memory barrier alone also wouldn't assure the (effective) atomicity of multiple operations inside the lock.
RickNZ
+2  A: 

Your assumption that the Pulse() call invokes a thread switch is not correct. It merely moves a thread from the wait queue to the ready queue. The Exit() call makes the switch, to the thread that's first in the ready queue.

Hans Passant
Thanks, but the scheduler can switch to a thread on the ready queue at any time, right? It doesn't need to wait for Exit(). So it can potentially switch to the other ready thread which will immediately hit the mutex, resulting in an other switch.
shojtsy
No, a thread in the ready queue is blocked on a native OS wait handle. It isn't eligible for scheduling until you release the lock.
Hans Passant