
On Aug 23, 2007, at 10:35 AM, Peter Dimov wrote:
On reflection though, I'll change the constructor from
explicit condition( Mutex * pm = 0 );
to
explicit condition( Mutex * pm );
as it's too easy to accidentally disable checking by not including the condition in the member init list.
Doesn't this remove goal 2? 2. The freedom to dynamically associate mutexes with condition variables. Here's the use case for it: You have dynamically allocated data protected by a mutex. Say that data represents something like a car bumper in a car assembly line. Perhaps we're programming an assembly robot. There is a thread that gets the data package (which includes the mutex) from a source. The threads job is to do something with the data, and then wait until another thread signals that it is ready to receive the processed data: condition<mutex> cv; ... while (true) { Data d = get_data(); unique_lock<mutex> lk = d.get_lock(); d.process(); while (d.not_ready_to_pass_on()) cv.wait(lk); stream << d; } In the above model, it is irrelevant that we're using the condition to wait on different mutexes in each iteration of the while loop. We work on the bumper, then wait until it is time to pass it to the next robot in the assembly line. Then we get a new bumper (or whatever). In the above use case, there is only one thread waiting on the condition. This makes it completely safe to keep rebinding a different mutex to the condition. With this use case, I believe we have to support goal #2 (which I credit Peter with bringing to my attention). That being said, a very similar use case to that above *is* an error. Now let's say that we have multiple threads such as that above, all using the same condition. As soon as two threads wait on the same condition at the same time, with different mutexes, we step into what POSIX calls undefined behavior. Though this is not necessarily a non- recoverable logic error. For example: We continue with the above use case, but make it a little more complicated. There are several threads getting the data, processing it somehow, and waiting on the same cv. But in this model, get_data() nearly always returns the same data, or at least the same associated mutex, guarding different values in the data. For example the data always represents a bumper of a specific make of car, but perhaps sometimes the bumper is red, sometimes white, sometimes with fog lights, sometimes without. struct controller { shared_ptr<Data> d_; shared_ptr<condition<mutex>> cv_; void initialize() {...} void process() { while (true) { d_ = get_data(); unique_lock<mutex> lk = d->get_lock(); while (d_->not_ready_to_process()) cv_->wait(lk); d_->process(); stream << *d_; } } void run() { while (true) { try { process(); } catch (condition_error&) { cv_ = get_condition(d_->get_lock()); } } } }; We now hold the condition in a shared_ptr as member data. It will almost never change value since get_data() nearly always returns the same mutex. But if get_data() does return a new mutex, then the condition::wait throws, that exception is caught, and the controller is essentially re-initialized with a new make of car. This software can even tolerate a "dynamic retooling" of the assembly line. That is, some threads may still be working on the last car model while other threads have finished their work and are already getting started on the new make of car. As the last thread working on the old model changes over, it destructs the old condition and acquires the new one. The above appears to be a reasonable and valid use case to me for "correcting" the condition/mutex binding. The retooling operation is a rare (exceptional) event. Using exceptions for this makes the main processing routine simpler/cleaner as it does not have to deal with the complication of retooling. In summary, we have valid use cases for both not checking condition/ mutex consistency, and for checking condition/mutex consistency, with both use cases built on "correctness" concerns as opposed to "performance" concerns. Where this leads us next, I'm not yet sure... -Howard