Boost logo

Boost :

From: Howard Hinnant (howard.hinnant_at_[hidden])
Date: 2007-08-25 11:53:09


On Aug 25, 2007, at 11:11 AM, Peter Dimov wrote:

> Howard Hinnant:
>
>> On Aug 22, 2007, at 11:55 AM, Howard Hinnant wrote:
>>
>>> Here's checked_condition:
>>
>> After investigating the use cases for the "unchecked condition", I
>> realized that my earlier checked_condition that I posted here wasn't
>> quite right, at least if you considered it a debug tool. To this end
>> I've rewritten it, and renamed it to condition_debug to emphasize its
>> role. The previous version was allowing default constructed
>> conditions to wait on different mutexes at the same time. This
>> revised condition_debug allows default constructed conditions to only
>> wait on different mutexes at different times. All simultaneous waits
>> must be on the same mutex (even for a default constructed condition).
>
> ...
>
> The checks seem too strict. It's valid to notify_all and then rebind
> the
> condition to a different mutex, but your condition_debug will likely
> assert/throw as the awakened threads have not yet executed their
> postchecks.
> (Unless I'm overlooking something.)
>
> It's quite likely that only the implementor of pthread_cond_t may
> catch all
> errors in the general case. This does not diminish the utility of a
> checked
> condition that catches most errors in the typical case IMO.

You're right. A (fully) debugging condition is at best extremely
difficult to implement. That's probably why POSIX made this behavior
undefined instead of specifying an error code return:

http://www.unix.org/single_unix_specification/

> When a thread waits on a condition variable, having specified a
> particular mutex to either the pthread_cond_timedwait() or the
> pthread_cond_wait() operation, a dynamic binding is formed between
> that
> mutex and condition variable that remains in effect as long as at
> least
> one thread is blocked on the condition variable. During this time, the
> effect of an attempt by any thread to wait on that condition variable
> using a different mutex is undefined. Once all waiting threads have
> been
> unblocked (as by the pthread_cond_broadcast() operation), the next
> wait
> operation on that condition variable shall form a new dynamic binding
> with the mutex specified by that wait operation. Even though the
> dynamic
> binding between condition variable and mutex may be removed or
> replaced
> between the time a thread is unblocked from a wait on the condition
> variable and the time that it returns to the caller or begins
> cancellation cleanup, the unblocked thread shall always re-acquire the
> mutex specified in the condition wait operation call from which it is
> returning.

I'm currently thinking that we should drop the condition(Mutex&)
constructor from condition:

     template <class Mutex>
     class condition
     {
     public:
         typedef Mutex mutex_type;

         condition();
         ~condition();

         condition(const condition&) = delete;
         condition& operator=(const condition&) = delete;

         void notify_one();
         void notify_all();
         template <class Lock>
             void wait(Lock& lock); // If !
lock.owns(), condition_error thrown
         template <class Lock, class Predicate>
             void wait(Lock& lock, Predicate pred); // If !
lock.owns(), condition_error thrown
         template <class Lock> // If !
lock.owns(), condition_error thrown
             bool timed_wait(Lock& lock, const utc_time& abs_time);
         template <class Lock, class Predicate> // If !
lock.owns(), condition_error thrown
             bool timed_wait(Lock& lock, const utc_time& abs_time,
Predicate pred);
     };

Adding the condition(Mutex&) constructor which does nothing but
specify undefined behavior provides no benefit, and quite possibly
adds cost to valid use cases of condition which use the default
constructor instead.

* The condition(Mutex&) constructor would not provide any guarantee
of safety.
* If an implementation did provide the error check, clients who had
valid use cases using the default constructor would pay for the error
checking even though they did not need or want it.
* A fully precise debugging condition appears difficult and/or
expensive.

(worst of both worlds)

The above being said, it is easy to add a "constrained condition" with
a variety of syntaxes and semantics. A "constrained condition" would
be one where the dynamic binding between the condition and the mutex
is set at construction time, and can not be altered throughout the
lifetime of the condition. The chief motivation for the constrained
condition is that the majority of use cases fit into the "constrained
use" pattern. I've shown a use case where the violation of this
binding is a correctable run time event, thus making an exception
appropriate. It might be appropriate to disallow default construction
of a constrained condition.

I can imagine several different syntaxes for a constrained condition:

condition<constrain<Mutex>> cv(mut);
constrain_condition<Mutex> cv(mut);
constrain::condition<Mutex> cv(mut);

All of the above syntaxes could be achieved either by the std::lib, or
by the client. Here is an example implementation of the first
syntax. It is fairly easy to write, and as far as I know, has no
subtleties.

template <class Mutex>
struct constrain {};

template <class Mutex>
class condition<constrain<Mutex>>
{
public:
     typedef Mutex mutex_type;

private:
     condition<mutex_type> cv_;
     mutex_type& ext_mut_;

public:
     explicit condition(mutex_type& mut) : ext_mut_(mut) {}

     void notify_one() {cv_.notify_one();}
     void notify_all() {cv_.notify_all();}
     template <class Lock>
         void wait(Lock& lock)
         {
             check(lock);
             cv_.wait(lock);
         }
     template <class Lock, class Predicate>
         void wait(Lock& lock, Predicate pred)
         {
             check(lock);
             cv_.wait(lock, std::move(pred));
         }
     template <class Lock>
         bool timed_wait(Lock& lock, const utc_time& abs_time)
         {
             check(lock);
             cv_.timed_wait(lock, abs_time);
         }
     template <class Lock, class Predicate>
         bool timed_wait(Lock& lock, const utc_time& abs_time,
Predicate pred)
         {
             check(lock);
             cv_.timed_wait(lock, abs_time, std::move(pred));
         }

private:
     template <class Lock>
         void check(const Lock& lock);
};

template <class Mutex>
template <class Lock>
void
condition<constrain<Mutex>>::check(const Lock& lock)
{
     if (lock.mutex() != &ext_mut_)
         throw condition_error();
}

The reason to make the constrained condition a different type from the
unconstrained condition is because the two have different semantics.
And there exist valid use cases for both semantics.

-Howard


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