Boost logo

Boost :

From: William Kempf (sirwillard_at_[hidden])
Date: 2000-08-19 10:24:41


--- In boost_at_[hidden], brent verner <brent_at_r...> wrote:
> On 18 Aug 2000 at 21:44 (-0000), William Kempf wrote:
> | >
> | > I agree with you. CV has a mutex. It would not be that
difficult to
> | > have two ctors for a CV, would it?
> | >
> | > basic_mutex* __m;
> | > bool __del_m;
> | > basic_condition() : __m( new basic_mutex() ) : __del_m(true)
{ }
> | > basic_condition( basic_mutex* m ) : __m( m ) : __del_m(false)
{ }
> |
> | No, this isn't difficult, but it doesn't address the problem.
The
> | mutex must actually be locked by a thread before it can call wait
()
> | or notify(). The wait() is the most obvious since it will do an
> | unlock/wait/lock operation. Since it's so obvious we immediately
> | tried to insure that we were locked at this point by simply
passing a
> | lock into the wait instead of passing in the mutex.
> |
> | The problem is, we also must insure that we're locked on the call
to
> | notify. This isn't as easy to solve, at least cleanly.
Basically,
> | the mutex and condition must have a one-to-many relationship (one
> | mutex to one or more conditions), so the mutex must be tied to
the CV
> | at construction. At this time it no longer makes sense to pass a
> | lock into wait() since we've already got access to the mutex,
though
> | we could do this just to insure we're locked at compile time. It
> | makes even less sense to pass a lock to notify(), though, since
notify
> | () won't effect the lock/mutex in any way. Further, passing it
in
> | like this is now an artificial improvement, since the lock may
well
> | not be a lock on the specific mutex instance that we're tied to.
>
> void
> basic_condition::wait()
> {
> __m->lock(); // calls pthread_mutex_lock( __m->real_mutex_t );
> pthread_cond_wait( this->real_cond_t, __m->real_mutex_t );
> pthread_mutex_unlock( __m->real_mutex_t );
> return;
> }

The call to wait should not lock the mutex. The whole point of a CV
is that the *user* will already have the mutex locked when he calls
wait and it will still be locked when you return from the wait... the
mutex is only temporarily unlocked within the call to wait.

void buffer::send(int a)
{
   mutex::lock lk(mx);
   while (full)
      cv.wait(lk);
   add_to_buffer(a);
}

The above psuedo code illustrates proper use of a CV. With your
implementation of wait we'll deadlock with non-recursive mutexes. If
we didn't lock in send() before doing a wait() then our check on the
predicate would be a race condition. Even checking the predicate
within wait() won't solve the problem because we'd then have a race
condition with the code that adds to our buffer. If we'd used a
recursive mutex your implementation would insure that the mutex was
locked during the call to pthread_cond_wait(), but wouldn't insure it
was locked during our wait() which would result in a no-gain.

> void
> basic_condition::notify()
> {
> __m->lock(); // lock or block...
> pthread_cond_signal(this->real_cond_t);
> return;
> }

With a recursive mutex this implementation solves the notify()
problem. However, we don't have a requirement for our mutexes to be
recursive. If the user had the mutex locked before calling notify()
and it was a non-recursive mutex we'd now have a deadlock.
 
> will the above not enforce necessary behavior for wait/notify using
> the class sketched above, or am I just failing to see the problem
(s)
> you are addressing altogether? -- in fact, I'm sure I don't
understand
> the problem you are trying to solve, could you post a little
snippet
> of (pseudo)code to demonstrate the situation that presents the great
> problem -- if there is already a fair discourse on this, please
point
> me in its direction.

The bounded buffer example posted numerous times here illustrates
proper use of CVs. If the CV is unlocked before you call wait()
you'll have a race condition (not to mention possibly worse problems
when wait() tries to unlock a mutex that's not locked and then later
locks the mutex before returning to the user). With notify() you
also have a race condition, but one that's much subtler and can lead
to "lost wake ups" or "spurious wakeups" which can lead to deadlock.
The mutex in question must be an external mutex, or at the very least
must be locked externally, for the CV to be used properly.
 
> | Or do we take the pthreads approach and simply have the
relationship
> | be an artificial one where if you don't follow the rules we
simply
> | have undefined behavior?
>
> no, you enforce the mutex state relationships _inside_ the condition
> class, where the mutex state. if not where is the benefit over
using
> the system's underlying thread implementation directly?

The state of the mutex *MUST* be changed externally. Using it
internally will result not in a CV, but a synch object more akin to a
Win32 event. A useful synch object, yes, but not for the same class
of problem domains. Whether or not the mutex is encapsulated within
the CV or simply shares a relationship with an external mutex doesn't
change the fact that the state has to be changed externally, yet must
follow precise rules during calls to wait() and notify().

Bill Kempf


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk