RE: Boost.Threads: Do we need all those mutexes?

Guys, Could we please resist complicating interfaces when we do not have to? I personally consider an additional blocking parameter for something like try_lock excessive because it distorts try_lock into becoming scoped_lock. Let's keep different concepts different. Blocking in try_lock is an oxymoron -- try_lock cannot block because it is "try". The basic usages (covering about 80%) are: scoped_lock l(m); // Block until it locks. scoped_try_lock l(m); // Try (!) to lock. I'll check the result. scoped_timed_lock l(m, time); // Block until it locks or time runs out. I'll check the result. Simple and clean. The only functionality that I can see beyond that is when I have to only declare a lock but apply it later. Like: Void Foo() { scoped_lock l(m, do_not_lock); if (something) { l.lock(); // Can't declare here. ... } } For that purpose we'll need something like scoped_lock(mutex&, nolock_t); That's it. Isn't it? Alternatively we might consider making timed_lock our basic lock as scoped_lock and try_lock are merely border-cases (time=infinite and time=0 respectively). Then, scoped_lock and try_lock would be real refinements (convenience interfaces) of that general concept. V. Christopher Currie wrote:
Michael Glassford wrote:
To address these problems, I would like to redefine the lock concept
constructors as follows (changes indicated with "^"):
Definitions ----------- L: lock type l: lock variable (type L) m: mutex variable (appropriate Mutex type) s: initial lock state (enum type {NO_LOCK=0, LOCK=1}) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ b: block? (enum type blocking_state {NON_BLOCKING=0, BLOCKING=1}) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Lock Concept ------------ L l(m,s) //Blocking lock if s L l(m) //Blocking lock
TryLock Concept --------------- L l(m,s,b) //Lock if s; blocking if b ^ ^^^^^^^^^^^^^ L l(m,b) //Lock; blocking if b ^ ^^^^^^^^^^^^^
TimedLock Concept ----------------- L l(m,s,b) //Lock if s; blocking if b ^ ^^^^^^^^^^^^^ L l(m,t) //Blocking lock, failing if not obtained by time t
I like this proposal, if for no other reason than the move to enums makes the arguments clearer. A couple of points:
Yes, I like LOCK and NO_LOCK better than true and false for the same reason.
TryLock: What would be the semantics of l(m, NO_LOCK, b)? In other words, if you're not going to lock on construction, why specify a blocking policy?
The only reason is so that if you write l(m, LOCK, ...) you could also specify the blocking parameter. An expansion of Vladimir Batov's of using structs rather than enums could help here: struct nolock_t {}; nolock_t nolock; struct lock_t {}; lock_t lock; class TryLock { TryLock(TryMutex m, no_lock_t s) {...} TryLock(TryMutex m, lock_t s, blocking_t b) {...} }
L l(m, b) and L l(m, s) seem to cover the two scenarios: either I don't want to lock, or I do and I need to specify
the blocking policy.
TimedLock: I'm not sure I follow the reasons why TimedMutex refines TryMutex. (This is a comment on the original design, not your specific changed, but bear with me.) I think a timeout of zero seconds achieves the same goal, even if TimedLock/TimedMutex don't implement the same operations. So I don't think that TimedLock needs a constructor that takes a blocking argument, and ultimately I think TimedLock/TimedMutex should refine Mutex/Lock, and not TryMutex/TryLock.
Also, the current implementation of TimedLock doesn't support a single argument constructor, even though it is supposedly a refinement of TryLock, which does. I'd like to see TimedLock implemented in such a way that I could use it as if it were a simple Lock, which I currently cannot. In summary, this is what I'd like to see:
Lock Concept --------------- L l(m) //Blocking lock L l(m,s) //Blocking lock if s
TryLock Concept --------------- As Lock, *plus:* L l(m,b) //Lock; blocking if b
TimedLock Concept ----------------- As Lock (not TryLock), *plus:* L l(m,t) //Blocking lock, failing if not obtained by time t //If t == 0, acts as TryLock.
As I said in another post, I'll think about this some more. Thanks for taking the time to comment. Mike

Batov, Vladimir wrote:
Guys,
Could we please resist complicating interfaces when we do not have to? I personally consider an additional blocking parameter for something like try_lock excessive because it distorts try_lock into becoming scoped_lock.
try_lock *is* scoped_lock, it just also supports a non-blocking locking operation.
Let's keep different concepts different. Blocking in try_lock is an oxymoron -- try_lock cannot block because it is "try".
What if I want it to? Just because it supports a non-blocking lock operation, doesn't mean it can't support a blocking one, and in some situations I may want to block on the mutex without having to create another lock object.
The basic usages (covering about 80%) are:
scoped_lock l(m); // Block until it locks. scoped_try_lock l(m); // Try (!) to lock. I'll check the result.
I disagree here, as well. Say I want to program generically... template <typename Mutex, typename Lock> class worker_class { Mutex m_; public: do_work() { Lock l(m_); // Does this call block? // am I guarenteed to be locked here? } } Yes, it's a contrived case, but the point is that all locks should have the same behavior in the 1-argument constructor case, and that is to block until this lock is obtained. The is the only way the user can use the lock in a generic way. -- Christopher Currie <codemonkey@gmail.com>

On Tuesday 29 June 2004 8:00 am, Christopher Currie wrote:
The basic usages (covering about 80%) are:
scoped_lock l(m); // Block until it locks. scoped_try_lock l(m); // Try (!) to lock. I'll check the result.
I disagree here, as well. Say I want to program generically...
template <typename Mutex, typename Lock> class worker_class { Mutex m_;
public: do_work() { Lock l(m_); // Does this call block? // am I guarenteed to be locked here? } }
Yes, it's a contrived case,
I don't believe it is a contrived case. Certainly once we have read/write mutexes we're going to want to tell generic components which kind of lock they need to use.
but the point is that all locks should have the same behavior in the 1-argument constructor case, and that is to block until this lock is obtained. The is the only way the user can use the lock in a generic way.
Absolutely. This is *extremely* important to keep in mind when discussing any kind of concept taxonomy. Refinements of a concept can only add restrictions, they cannot remove them. Doug

Doug Gregor wrote:
On Tuesday 29 June 2004 8:00 am, Christopher Currie wrote:
The basic usages (covering about 80%) are:
scoped_lock l(m); // Block until it locks. scoped_try_lock l(m); // Try (!) to lock. I'll check the result.
I disagree here, as well. Say I want to program generically...
template <typename Mutex, typename Lock> class worker_class { Mutex m_;
public: do_work() { Lock l(m_); // Does this call block? // am I guarenteed to be locked here? } }
Yes, it's a contrived case,
I don't believe it is a contrived case. Certainly once we have read/write mutexes we're going to want to tell generic components which kind of lock they need to use.
I was going to ask if locks were likely to be used generically in this way; obviously they will, in your opinion. I appreciate your bringing up read/write locks; I was planning to bring them into the discussion as well.
but the point is that all locks should have the same behavior in the 1-argument constructor case, and that is to block until this lock is obtained. The is the only way the user can use the lock in a generic way.
Absolutely. This is *extremely* important to keep in mind when discussing any kind of concept taxonomy.
I would consider this more an argument for eliminating 1-argument constructors entirely (except for scoped_lock, where its meaning is completely unambiguous): there doesn't seem to be a way to define them that is both consistent within the lock class and consistent across lock classes (e.g. I agree with Vladimir that for consistency within the try_lock class, it should be non-blocking; but as Christopher pointed out, for consistency across lock classes, it should be blocking). And, if you consider the larger case, which also includes read/write locks, single-argument constructors are even harder to define (do the read-lock or write-lock?; do they block or not?).
Refinements of a concept can only add restrictions, they cannot remove them.
True. But it's not hard to define the concepts in such a way that the lock classes can have different constructors: Lock concept defines all lock operations except constructors. TryLock concept refines Lock by adding try_* methods. TimedLock concept refines TryLock by adding timed_* methods. ScopedLock refines Lock by adding appropriate constructors. ScopedTryLock refines TryLock by adding appropriate constructors. ScopedTimedLock refines TimedLock by adding appropriate constructors. Mike

On Tuesday 29 June 2004 9:15 am, Michael Glassford wrote:
Doug Gregor wrote:
but the point is that all locks should have the same behavior in the 1-argument constructor case, and that is to block until this lock is obtained. The is the only way the user can use the lock in a generic way.
Absolutely. This is *extremely* important to keep in mind when discussing any kind of concept taxonomy.
I would consider this more an argument for eliminating 1-argument constructors entirely (except for scoped_lock, where its meaning is completely unambiguous): there doesn't seem to be a way to define them that is both consistent within the lock class and consistent across lock classes (e.g. I agree with Vladimir that for consistency within the try_lock class, it should be non-blocking; but as Christopher pointed out, for consistency across lock classes, it should be blocking).
Right, so what we have (I think) are reasonably well-designed classes with incorrectly specified concepts. scoped_lock should really lock; try_lock should just try to lock.
And, if you consider the larger case, which also includes read/write locks, single-argument constructors are even harder to define (do the read-lock or write-lock?; do they block or not?).
I'd expect scoped_*_read_lock and scoped_*_write_lock classes to be separate classes that model whatever locking concepts we come up with. Actually, the design of the read/write lock on the branch really surprised me, because it used the read_write_lock_state enum instead of distinct types.
Refinements of a concept can only add restrictions, they cannot remove them.
True. But it's not hard to define the concepts in such a way that the lock classes can have different constructors:
Lock concept defines all lock operations except constructors. TryLock concept refines Lock by adding try_* methods. TimedLock concept refines TryLock by adding timed_* methods.
ScopedLock refines Lock by adding appropriate constructors. ScopedTryLock refines TryLock by adding appropriate constructors. ScopedTimedLock refines TimedLock by adding appropriate constructors.
Looks perfectly reasonable to me. Doug

Doug Gregor <dgregor@cs.indiana.edu> writes:
Right, so what we have (I think) are reasonably well-designed classes with incorrectly specified concepts. scoped_lock should really lock; try_lock should just try to lock.
I don't understand; are you addressing the concept problem above? scoped_lock refers to a class, right? -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

On Tuesday 29 June 2004 10:21 am, David Abrahams wrote:
Doug Gregor <dgregor@cs.indiana.edu> writes:
Right, so what we have (I think) are reasonably well-designed classes with incorrectly specified concepts. scoped_lock should really lock; try_lock should just try to lock.
I don't understand; are you addressing the concept problem above? scoped_lock refers to a class, right?
No and yes :) At this point, we know that the concepts are inconsistent with the code. I'm just saying that the concepts are wrong, and that the existing semantics of the classes scoped_lock and try_lock (actually, it's called scoped_try_lock) are the correct ones. Doug

Doug Gregor wrote:
On Tuesday 29 June 2004 9:15 am, Michael Glassford wrote:
Doug Gregor wrote:
but the point is that all locks should have the same behavior in the 1-argument constructor case, and that is to block until this lock is obtained. The is the only way the user can use the lock in a generic way.
Absolutely. This is *extremely* important to keep in mind when discussing any kind of concept taxonomy.
I would consider this more an argument for eliminating 1-argument constructors entirely (except for scoped_lock, where its meaning is completely unambiguous): there doesn't seem to be a way to define them that is both consistent within the lock class and consistent across lock classes (e.g. I agree with Vladimir that for consistency within the try_lock class, it should be non-blocking; but as Christopher pointed out, for consistency across lock classes, it should be blocking).
Right, so what we have (I think) are reasonably well-designed classes with incorrectly specified concepts. scoped_lock should really lock; try_lock should just try to lock.
I'm not sure exactly what you're suggesting here. Are you talking about only the constructors, or would you completely eliminate all blocking functionality (e.g. the lock() method) from try_lock? And all non-timed functionality (e.g. the lock() and try_lock() methods) from timed_lock?
And, if you consider the larger case, which also includes read/write locks, single-argument constructors are even harder to define (do the read-lock or write-lock?; do they block or not?).
I'd expect scoped_*_read_lock and scoped_*_write_lock classes to be separate classes that model whatever locking concepts we come up with. Actually, the design of the read/write lock on the branch really surprised me, because it used the read_write_lock_state enum instead of distinct types.
There's a lot to be said for separate read-lock and write-lock types, and I was hoping to address that at some point; however, it would make lock promotion/demotion more difficult (unless maybe the read_lock class had a promote() method that returned a write_lock class or something like that, but that has some problems, too).
Refinements of a concept can only add restrictions, they cannot remove them.
True. But it's not hard to define the concepts in such a way that the lock classes can have different constructors:
Lock concept defines all lock operations except constructors. TryLock concept refines Lock by adding try_* methods. TimedLock concept refines TryLock by adding timed_* methods.
ScopedLock refines Lock by adding appropriate constructors. ScopedTryLock refines TryLock by adding appropriate constructors. ScopedTimedLock refines TimedLock by adding appropriate constructors.
Looks perfectly reasonable to me.
Doug
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost

On Tuesday 29 June 2004 10:55 am, Michael Glassford wrote:
Doug Gregor wrote:
Right, so what we have (I think) are reasonably well-designed classes with incorrectly specified concepts. scoped_lock should really lock; try_lock should just try to lock.
I'm not sure exactly what you're suggesting here. Are you talking about only the constructors, or would you completely eliminate all blocking functionality (e.g. the lock() method) from try_lock? And all non-timed functionality (e.g. the lock() and try_lock() methods) from timed_lock?
I see that my comment was horribly ambiguous :) Let's try to forget I said it, and I'll pipe in elsewhere more clearly with suggestions.
And, if you consider the larger case, which also includes read/write locks, single-argument constructors are even harder to define (do the read-lock or write-lock?; do they block or not?).
I'd expect scoped_*_read_lock and scoped_*_write_lock classes to be separate classes that model whatever locking concepts we come up with. Actually, the design of the read/write lock on the branch really surprised me, because it used the read_write_lock_state enum instead of distinct types.
There's a lot to be said for separate read-lock and write-lock types, and I was hoping to address that at some point; however, it would make lock promotion/demotion more difficult (unless maybe the read_lock class had a promote() method that returned a write_lock class or something like that, but that has some problems, too).
Could write_lock have a constructor/method that takes in the read lock to be promoted? I'm a little fuzzy on the implementation issues here. Doug

Doug Gregor wrote:
On Tuesday 29 June 2004 10:55 am, Michael Glassford wrote:
Doug Gregor wrote:
[snip]
And, if you consider the larger case, which also includes read/write locks, single-argument constructors are even harder to define (do the read-lock or write-lock?; do they block or not?).
I'd expect scoped_*_read_lock and scoped_*_write_lock classes to be separate classes that model whatever locking concepts we come up with. Actually, the design of the read/write lock on the branch really surprised me, because it used the read_write_lock_state enum instead of distinct types.
There's a lot to be said for separate read-lock and write-lock types, and I was hoping to address that at some point; however, it would make lock promotion/demotion more difficult (unless maybe the read_lock class had a promote() method that returned a write_lock class or something like that, but that has some problems, too).
Could write_lock have a constructor/method that takes in the read lock to be promoted? I'm a little fuzzy on the implementation issues here.
I suppose it could, but if you needed to promote and demote a lock a couple of times it could get messy; and if you needed to promote conditionally, it could also get messy. Also, you could end up with some interesting situations like this: void f(read_write_mutex m) { read_write_mutex::read_lock r(m); if (...) { read_write_mutex::write_lock w(r); //lock promotion //... } //Point A } The most obvious implementation of promotion would be for the write lock to unlock the read lock if promotion succeeded, but leave it locked if promotion failed. But in the above code, this would mean that if promotion succeeds, neither lock will be locked at "Point A"; however if promotion fails, r will still be read-locked at point A. Another possibility is to define separate read_lock, write_lock, and read_write_lock classes, and only the last would allow promotion & demotion. The disadvantage of this is a proliferation of lock classes, but it may be the best option, all things considered. Mike

Michael Glassford wrote:
Also, you could end up with some interesting situations like this:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); if (...) { read_write_mutex::write_lock w(r); //lock promotion //... } //Point A }
The most obvious implementation of promotion would be for the write lock to unlock the read lock if promotion succeeded, but leave it locked if promotion failed. But in the above code, this would mean that if promotion succeeds, neither lock will be locked at "Point A"; however if promotion fails, r will still be read-locked at point A.
Not necessarily, ~write_lock can (should) demote the lock back to read (or whatever the initial condition of r was).

Peter Dimov wrote:
Michael Glassford wrote:
Also, you could end up with some interesting situations like this:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); if (...) { read_write_mutex::write_lock w(r); //lock promotion //... } //Point A }
The most obvious implementation of promotion would be for the write lock to unlock the read lock if promotion succeeded, but leave it locked if promotion failed. But in the above code, this would mean that if promotion succeeds, neither lock will be locked at "Point A"; however if promotion fails, r will still be read-locked at point A.
Not necessarily, ~write_lock can (should) demote the lock back to read (or whatever the initial condition of r was).
True. But then, in the reverse case: void f(read_write_mutex m) { read_write_mutex::write_lock w(m); if (...) { read_write_mutex::read_lock r(w); //lock demotion //... } //Point A } It may not be possible for r's destructor to re-obtain the write lock, which means that, at point A: 1) If the write lock can be re-obtained, w is write-locked. 2) Otherwise, w is either unlocked or read-locked. The idea of a write-lock being read-locked is pretty strange, but having it unlocked doesn't seem a good choice, either. I realize that a combined read-write lock has the same problem of possibly being unable to re-obtain the write-lock, but in that case it is at least more obvious that the lock state changed and what it changed to: void f(read_write_mutex m) { read_write_mutex::read_write_lock l(m, write_locked); if (...) { l.demote(); //lock demotion //... } //Point A: l is read-locked } Or: void f(read_write_mutex m) { read_write_mutex::read_write_lock l(m, write_locked); if (...) { l.demote(); //lock demotion //... l.try_promote(); //optional } //Point A: l is write-locked if try_promote() succeeded, //read-locked if it failed; never unlocked. } Finally, to take two more realistic use cases: void f(read_write_mutex m) { read_write_mutex::write_lock w(m); //... read_write_mutex::read_lock r(w); //lock promotion //... } And: void f(read_write_mutex m) { read_write_mutex::write_lock r(m); //... read_write_mutex::read_lock w(r); //lock demotion //... } The second lock does unnecessary work in demoting/promoting the lock for the first lock, which then immediately unlocks it. Don't get me wrong: I actually like the idea of separate read and write locks, but there seem to be some tricky problems to address to make them work with lock promotion/demotion. Mike

Michael Glassford <glassfordm@hotmail.com> writes:
Peter Dimov wrote:
Michael Glassford wrote:
Also, you could end up with some interesting situations like this:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); if (...) { read_write_mutex::write_lock w(r); //lock promotion //... } //Point A }
The most obvious implementation of promotion would be for the write lock to unlock the read lock if promotion succeeded, but leave it locked if promotion failed. But in the above code, this would mean that if promotion succeeds, neither lock will be locked at "Point A"; however if promotion fails, r will still be read-locked at point A. Not necessarily, ~write_lock can (should) demote the lock back to read (or whatever the initial condition of r was).
True. But then, in the reverse case:
void f(read_write_mutex m) { read_write_mutex::write_lock w(m); if (...) { read_write_mutex::read_lock r(w); //lock demotion //... } //Point A }
It may not be possible for r's destructor to re-obtain the write lock
I don't think this usage should be allowed. Once you have a write lock (the "higher class" of lock), then you keep your write lock until you release it. If you want a read lock for the whole time, but only a write lock for a portion of that time, then you write code like the first example. Your "reverse case" actually fulfils that requirement, since there is a portion of the code that only needs a read lock. Therefore it should look like: void f(read_write_mutex m) { read_write_mutex::read_lock r(m); // we need at least a read lock // for everything { read_write_mutex::write_lock w(r); // do stuff that needs the write lock here } if (...) { // we only need the read lock here //... } { read_write_mutex::write_lock w(r); // do more stuff that needs the write lock here } } Anthony -- Anthony Williams Senior Software Engineer, Beran Instruments Ltd.

Anthony Williams wrote:
Michael Glassford <glassfordm@hotmail.com> writes:
Peter Dimov wrote:
Michael Glassford wrote:
Also, you could end up with some interesting situations like this:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); if (...) { read_write_mutex::write_lock w(r); //lock promotion //... } //Point A }
The most obvious implementation of promotion would be for the write lock to unlock the read lock if promotion succeeded, but leave it locked if promotion failed. But in the above code, this would mean that if promotion succeeds, neither lock will be locked at "Point A"; however if promotion fails, r will still be read-locked at point A.
Not necessarily, ~write_lock can (should) demote the lock back to read (or whatever the initial condition of r was).
True. But then, in the reverse case:
void f(read_write_mutex m) { read_write_mutex::write_lock w(m); if (...) { read_write_mutex::read_lock r(w); //lock demotion //... } //Point A }
It may not be possible for r's destructor to re-obtain the write lock
I don't think this usage should be allowed.
How would it be prevented?
Once you have a write lock (the "higher class" of lock), then you keep your write lock until you release it. If you want a read lock for the whole time, but only a write lock for a portion of that time, then you write code like the first example. Your "reverse case" actually fulfils that requirement, since there is a portion of the code that only needs a read lock. Therefore it should look like:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); // we need at least a read lock // for everything { read_write_mutex::write_lock w(r); // do stuff that needs the write lock here } if (...) { // we only need the read lock here //... } { read_write_mutex::write_lock w(r); // do more stuff that needs the write lock here } }
Again, this might be a better way to write it, but how do you prevent someone from writing it the first way? I think one of the most common use cases for lock demotion would be: obtain a write lock and make modifications to a resource; demote to a read lock to release other readers and use the resource, making sure that it is still in the state it was when we had the write lock (i.e., that no other thread obtained a write lock and changed it between the time we released our write lock and the time we obtained our read lock). With a read/write lock, this is expressed in a straightforward way: void f(read_write_mutex m) { read_write_mutex::write_lock l(m); //Modify the resource l.demote(); //Release other readers //Use the resource } With the separate read-lock and write-lock, how do you express this in a way that is straightforward, doesn't do unnecessary work when the locks are released, etc.? Mike

Michael Glassford <glassfordm@hotmail.com> writes:
Anthony Williams wrote:
Michael Glassford <glassfordm@hotmail.com> writes:
True. But then, in the reverse case:
void f(read_write_mutex m) { read_write_mutex::write_lock w(m); if (...) { read_write_mutex::read_lock r(w); //lock demotion //... } //Point A }
It may not be possible for r's destructor to re-obtain the write lock I don't think this usage should be allowed.
How would it be prevented?
Don't allow a read_lock to be constructed from a write_lock.
Once you have a write lock (the "higher class" of lock), then you keep your write lock until you release it. If you want a read lock for the whole time, but only a write lock for a portion of that time, then you write code like the first example. Your "reverse case" actually fulfils that requirement, since there is a portion of the code that only needs a read lock. Therefore it should look like:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); // we need at least a read lock // for everything { read_write_mutex::write_lock w(r); // do stuff that needs the write lock here } if (...) { // we only need the read lock here //... } { read_write_mutex::write_lock w(r); // do more stuff that needs the write lock here } }
Again, this might be a better way to write it, but how do you prevent someone from writing it the first way?
See above.
I think one of the most common use cases for lock demotion would be: obtain a write lock and make modifications to a resource; demote to a read lock to release other readers and use the resource, making sure that it is still in the state it was when we had the write lock (i.e., that no other thread obtained a write lock and changed it between the time we released our write lock and the time we obtained our read lock).
With a read/write lock, this is expressed in a straightforward way:
void f(read_write_mutex m) { read_write_mutex::write_lock l(m);
//Modify the resource
l.demote(); //Release other readers
//Use the resource }
With the separate read-lock and write-lock, how do you express this in a way that is straightforward, doesn't do unnecessary work when the locks are released, etc.?
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); { read_write_mutex::write_lock l(r); //Modify the resource } // release other readers //Use the resource } Anthony -- Anthony Williams Senior Software Engineer, Beran Instruments Ltd.

Anthony Williams wrote:
Michael Glassford <glassfordm@hotmail.com> writes:
Anthony Williams wrote:
Michael Glassford <glassfordm@hotmail.com> writes:
True. But then, in the reverse case:
void f(read_write_mutex m) { read_write_mutex::write_lock w(m); if (...) { read_write_mutex::read_lock r(w); //lock demotion //... } //Point A }
It may not be possible for r's destructor to re-obtain the write lock
I don't think this usage should be allowed.
How would it be prevented?
Don't allow a read_lock to be constructed from a write_lock.
Thinking about it some more after replying, I thought that's what you might mean; however, see my comment on your example below.
Once you have a write lock (the "higher class" of lock), then you keep your write lock until you release it. If you want a read lock for the whole time, but only a write lock for a portion of that time, then you write code like the first example. Your "reverse case" actually fulfils that requirement, since there is a portion of the code that only needs a read lock. Therefore it should look like:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); // we need at least a read lock // for everything { read_write_mutex::write_lock w(r); // do stuff that needs the write lock here } if (...) { // we only need the read lock here //... } { read_write_mutex::write_lock w(r); // do more stuff that needs the write lock here } }
Again, this might be a better way to write it, but how do you prevent someone from writing it the first way?
See above.
The above code has a problem, however. This: void f(read_write_mutex m) { read_write_mutex::read_lock w(m); //Block until we get read-lock //do stuff read_write_mutex::write_lock(r); //Convert to write-lock; may fail; then what? //do more stuff } Is not the same as this: void f(read_write_mutex m) { read_write_mutex::read_write_lock l(m, write_locked); //Block until we get write-lock //do stuff l.demote(); //Convert to read-lock; never fails //do more stuff } The reason is that the first example always succeeds in getting the write lock eventually, and lock demotion always succeeds as well, so we always get the lock we want. In the second example, we can always get the read-lock, but the lock promotion may fail, so we may never get the write lock. The reason lock promotion may fail is that if two threads holding read locks are both waiting for promotion to write-locks, neither will ever get the write lock (which can't be obtained until all other read-locks have been released) and they will deadlock. So, unless you know of a better solution, at most one thread must ever be allowed to wait for lock promotion and any other promotion attempts must fail.
I think one of the most common use cases for lock demotion would be: obtain a write lock and make modifications to a resource; demote to a read lock to release other readers and use the resource, making sure that it is still in the state it was when we had the write lock (i.e., that no other thread obtained a write lock and changed it between the time we released our write lock and the time we obtained our read lock).
With a read/write lock, this is expressed in a straightforward way:
void f(read_write_mutex m) { read_write_mutex::write_lock l(m);
//Modify the resource
l.demote(); //Release other readers
//Use the resource }
With the separate read-lock and write-lock, how do you express this in a way that is straightforward, doesn't do unnecessary work when the locks are released, etc.?
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); { read_write_mutex::write_lock l(r);
//Modify the resource } // release other readers
//Use the resource }
This has the same problem I mention above. Unless I'm missing something, it also performs unnecessary work (a promotion followed by a demotion, as opposed to only a demotion in my example). Mike

Michael Glassford <glassfordm@hotmail.com> writes:
Anthony Williams wrote:
Michael Glassford <glassfordm@hotmail.com> writes:
Anthony Williams wrote:
Once you have a write lock (the "higher class" of lock), then you keep your write lock until you release it. If you want a read lock for the whole time, but only a write lock for a portion of that time, then you write code like the first example. Your "reverse case" actually fulfils that requirement, since there is a portion of the code that only needs a read lock. Therefore it should look like:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); // we need at least a read lock // for everything { read_write_mutex::write_lock w(r); // do stuff that needs the write lock here } if (...) { // we only need the read lock here //... } { read_write_mutex::write_lock w(r); // do more stuff that needs the write lock here } }
Again, this might be a better way to write it, but how do you prevent someone from writing it the first way? See above.
The above code has a problem, however. This:
void f(read_write_mutex m) { read_write_mutex::read_lock w(m); //Block until we get read-lock
//do stuff
read_write_mutex::write_lock(r); //Convert to write-lock; may fail; then what?
Don't allow it to fail --- block.
//do more stuff }
Is not the same as this:
void f(read_write_mutex m) { read_write_mutex::read_write_lock l(m, write_locked); //Block until we get write-lock
//do stuff
l.demote(); //Convert to read-lock; never fails
//do more stuff }
The reason is that the first example always succeeds in getting the write lock eventually, and lock demotion always succeeds as well, so we always get the lock we want. In the second example, we can always get the read-lock, but the lock promotion may fail, so we may never get the write lock.
Not an issue if promotion is blocking.
The reason lock promotion may fail is that if two threads holding read locks are both waiting for promotion to write-locks, neither will ever get the write lock (which can't be obtained until all other read-locks have been released) and they will deadlock. So, unless you know of a better solution, at most one thread must ever be allowed to wait for lock promotion and any other promotion attempts must fail.
In which case, don't do it like that. I see this as the fundamental problem of promotion --- if the idea of promotion is to ensure that no one has modified the data since you read it, then if two threads have read locks and try and promote them to write locks, either you deadlock or one will fail. If it fails, what do you do? You have to release your read lock to let the other thread do the write, and then try again. This makes me think that such a feature is not particularly useful in the general case. Maybe a try_write_lock variant is in order? I might code it so that the write_lock does the release/reacquire itself, but this is a difficult issue. Allowing deadlocks is not a good thing, but I really don't like the idea of the try_xxx locks because it raises the issue of "have I got the lock or not?". I'm also not a fan of control by member functions --- this is a resource, so we should use RAII.
I think one of the most common use cases for lock demotion would be: obtain a write lock and make modifications to a resource; demote to a read lock to release other readers and use the resource, making sure that it is still in the state it was when we had the write lock (i.e., that no other thread obtained a write lock and changed it between the time we released our write lock and the time we obtained our read lock).
With a read/write lock, this is expressed in a straightforward way:
void f(read_write_mutex m) { read_write_mutex::write_lock l(m);
//Modify the resource
l.demote(); //Release other readers
//Use the resource }
With the separate read-lock and write-lock, how do you express this in a way that is straightforward, doesn't do unnecessary work when the locks are released, etc.? void f(read_write_mutex m) { read_write_mutex::read_lock r(m); { read_write_mutex::write_lock l(r); //Modify the resource } // release other readers //Use the resource }
This has the same problem I mention above. Unless I'm missing something, it also performs unnecessary work (a promotion followed by a demotion, as opposed to only a demotion in my example).
I agree there is the possibility for unnecessary work, but in my view it clearly marks the region where writing is done. With a read-write lock, I can write: void g(read_write_mutex::read_write_lock& l); void f(read_write_mutex& m) { read_write_mutex::read_write_lock l(m,write_locked); g(l); // Are we still locked for writing, or just reading? } void f2(read_write_mutex& m) { read_write_mutex::read_write_lock l(m,read_locked); g(l); // Are we still locked for just reading, or for writing as well? } Whereas with separate locks it's clear what's happening when I write: void g(read_write_mutex::read_lock& l); void g(read_write_mutex::write_lock& l); void f(read_write_mutex& m) { read_write_mutex::read_lock l(m); g(l); // We're still locked for just reading, whether or not g did writing } void f2(read_write_mutex& m) { read_write_mutex::write_lock l(m); g(l); // We're still locked for writing } Anthony -- Anthony Williams Senior Software Engineer, Beran Instruments Ltd.

Anthony Williams wrote:
Michael Glassford <glassfordm@hotmail.com> writes:
Anthony Williams wrote:
Michael Glassford <glassfordm@hotmail.com> writes:
Anthony Williams wrote:
Once you have a write lock (the "higher class" of lock), then you keep your write lock until you release it. If you want a read lock for the whole time, but only a write lock for a portion of that time, then you write code like the first example. Your "reverse case" actually fulfils that requirement, since there is a portion of the code that only needs a read lock. Therefore it should look like:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); // we need at least a read lock // for everything { read_write_mutex::write_lock w(r); // do stuff that needs the write lock here } if (...) { // we only need the read lock here //... } { read_write_mutex::write_lock w(r); // do more stuff that needs the write lock here } }
Again, this might be a better way to write it, but how do you prevent someone from writing it the first way?
See above.
The above code has a problem, however. This:
void f(read_write_mutex m) { read_write_mutex::read_lock w(m); //Block until we get read-lock
//do stuff
read_write_mutex::write_lock(r); //Convert to write-lock; may fail; then what?
Don't allow it to fail --- block.
That's a bad idea because of deadlock issues, as I (and you) mention below. And in any case, the two pieces of code are still not the same. The first will often block or deadlock at the promotion, the second will never block at the demotion.
//do more stuff }
Is not the same as this:
void f(read_write_mutex m) { read_write_mutex::read_write_lock l(m, write_locked); //Block until we get write-lock
//do stuff
l.demote(); //Convert to read-lock; never fails
//do more stuff }
The reason is that the first example always succeeds in getting the write lock eventually, and lock demotion always succeeds as well, so we always get the lock we want. In the second example, we can always get the read-lock, but the lock promotion may fail, so we may never get the write lock.
Not an issue if promotion is blocking.
The reason lock promotion may fail is that if two threads holding read locks are both waiting for promotion to write-locks, neither will ever get the write lock (which can't be obtained until all other read-locks have been released) and they will deadlock. So, unless you know of a better solution, at most one thread must ever be allowed to wait for lock promotion and any other promotion attempts must fail.
In which case, don't do it like that.I see this as the fundamental problem of promotion
I agree. Which is why I recommended getting the write lock first and later demoting it, as opposed to the solution you originally suggested which requires getting the read-lock first and then promoting it.
if the idea of promotion is to ensure that no one has modified the data since you read it, then if two threads have read locks and try and promote them to write locks, either you deadlock or one will fail. If it fails, what do you do? You have to release your read lock to let the other thread do the write, and then try again.
There are times when even this could be useful for efficiency, I suppose--you try to promote first, then if it fails you release the read-lock, get a write-lock, redo (as little as possible of) the work you did when you had the read-lock, then do the writing. However... In my mind demotion is more important: you make modifications, then demote to a read-lock to allow other readers access to the resource while you continue to read from it yourself, with the guarantee that the changes you just made have not been modified by another thread before you use them.
This makes me think that such a feature is not particularly useful in the general case. Maybe a try_write_lock variant is in order?
Are you referring to one that implements something like try_promote()? If so, I agree to the point that I have so far provided only try_promote() and timed_promote() functions in Boost.Threads, but no promote() function at all due to the problems we both raised above.
I might code it so that the write_lock does the release/reacquire itself, but this is a difficult issue.
I have an experimental set_lock member in the read/write locks that does this (release/reacquire): it changes from one lock state to another, using the best option possible. For example, when changing from a read-lock to a write-lock, it first uses try_promote() (if available); if that fails, it unlocks then write-locks. I thought it also returned a value that indicated whether it had to release the lock first, but see that it doesn't; maybe I need to change that.
Allowing deadlocks is not a good thing, but I really don't like the idea of the try_xxx locks because it raises the issue of "have I got the lock or not?". I'm also not a fan of control by member functions --- this is a resource, so we should use RAII.
It is using RAII, at least as much as is commonly done. It's pretty common to acquire the resource in the constructor, change its state with a member function, and release it (if applicable) in the destructor. The lock() and unlock() methods of the Boost.Threads lock classes are another example of the same thing, as are smart pointer assignment operators, etc. It would be pretty limiting to remove all these.
I think one of the most common use cases for lock demotion would be: obtain a write lock and make modifications to a resource; demote to a read lock to release other readers and use the resource, making sure that it is still in the state it was when we had the write lock (i.e., that no other thread obtained a write lock and changed it between the time we released our write lock and the time we obtained our read lock).
With a read/write lock, this is expressed in a straightforward way:
void f(read_write_mutex m) { read_write_mutex::write_lock l(m);
//Modify the resource
l.demote(); //Release other readers
//Use the resource }
With the separate read-lock and write-lock, how do you express this in a way that is straightforward, doesn't do unnecessary work when the locks are released, etc.?
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); { read_write_mutex::write_lock l(r); //Modify the resource } // release other readers //Use the resource }
This has the same problem I mention above. Unless I'm missing something, it also performs unnecessary work (a promotion followed by a demotion, as opposed to only a demotion in my example).
I agree there is the possibility for unnecessary work, but in my view it clearly marks the region where writing is done. With a read-write lock, I can write:
void g(read_write_mutex::read_write_lock& l);
void f(read_write_mutex& m) { read_write_mutex::read_write_lock l(m,write_locked); g(l); // Are we still locked for writing, or just reading? }
void f2(read_write_mutex& m) { read_write_mutex::read_write_lock l(m,read_locked); g(l); // Are we still locked for just reading, or for writing as well?
True. The same is true of the unlock(() and lock() members of lock classes, the assignment operator of smart pointers, and, in general, of any function that can modify its arguments. What if you write: void g(const read_write_mutex::read_write_lock& l); instead?
}
Whereas with separate locks it's clear what's happening when I write:
void g(read_write_mutex::read_lock& l); void g(read_write_mutex::write_lock& l);
void f(read_write_mutex& m) { read_write_mutex::read_lock l(m); g(l); // We're still locked for just reading, whether or not g did writing }
void f2(read_write_mutex& m) { read_write_mutex::write_lock l(m); g(l); // We're still locked for writing }
I agree completely that separate locks are a good idea in most cases, but don't think that they work very well with lock promotion or demotion. What I'm most inclined to do at the moment is to have separate read_lock and write_lock classes that don't support promotion or demotion, and a combined read_write_lock class that supports both for those cases where promotion and demotion are required. Mike

Michael Glassford wrote:
Finally, to take two more realistic use cases:
void f(read_write_mutex m) { read_write_mutex::write_lock w(m);
//...
read_write_mutex::read_lock r(w); //lock promotion
//... }
And:
void f(read_write_mutex m) { read_write_mutex::write_lock r(m);
//...
read_write_mutex::read_lock w(r); //lock demotion
//... }
The second lock does unnecessary work in demoting/promoting the lock for the first lock, which then immediately unlocks it.
This is a very good point, since in the second example the unnecessary promotion would block. This seems to suggest that read_write_lock is necessary in such a scenario.

Peter Dimov wrote:
Michael Glassford wrote:
Finally, to take two more realistic use cases:
void f(read_write_mutex m) { read_write_mutex::write_lock w(m);
//...
read_write_mutex::read_lock r(w); //lock promotion
//... }
And:
void f(read_write_mutex m) { read_write_mutex::write_lock r(m);
//...
read_write_mutex::read_lock w(r); //lock demotion
//... }
The second lock does unnecessary work in demoting/promoting the lock for the first lock, which then immediately unlocks it.
This is a very good point, since in the second example the unnecessary promotion would block.
One thing that I haven't made clear is that (so far at least) the Boost.Threads read/write mutex doesn't support infinitely-blocking promotion, which has problems (e.g. if two threads with read locks block waiting to be promoted to write locks, they deadlock); it only supports try_promote() and timed_promote(). So there shouldn't be a problem with blocking, at least, in the second example.
This seems to suggest that read_write_lock is necessary in such a scenario.
Looks like I really messed up my examples; I'm glad you could see my point in spite of that. Of course, what I meant was this: void f(read_write_mutex m) { read_write_mutex::read_lock r(m); //... read_write_mutex::write_lock w(r); //lock promotion //... } And: void f(read_write_mutex m) { read_write_mutex::write_lock w(m); //... read_write_mutex::read_lock r(w); //lock demotion //... } Mike

On Jun 29, 2004, at 12:30 PM, Michael Glassford wrote:
Also, you could end up with some interesting situations like this:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m); if (...) { read_write_mutex::write_lock w(r); //lock promotion //... } //Point A }
How 'bout: void f(read_write_mutex& m) { read_write_mutex::read_lock r(m); if (...) { r.unlock(); read_write_mutex::write_lock w(m); //... m is write locked w.transfer_to_read_lock(r); } //Point A ... m is read locked } Transferring mutex ownership from a read_lock to a write_lock is always a blocking operation. So there's no advantage to a transfer_to_write_lock() function. You might as well manually unlock the read_lock and manually lock the write_lock as shown above. But the write_lock::transfer_to_read_lock() member can atomically transfer ownership to the appointed read_lock and notify all other read_lock's that it is time to wake up. Summary: 1. There's only two types of locks: read_lock, write_lock. 2. Exception safety is maintained. 3. Interface is minimal and intuitive (at least to me). 4. r is always locked at Point A. 5. If the write_lock branch is taken, the code can assume that at Point A the thread is reading what was written while the write_lock was held. There is no chance for another thread to write. 6. There is no chance for deadlock of multiple read_locks waiting for promotion because there is no such operation. You must unlock the read_lock and then block for a write_lock. -Howard PS: I find the promotion/demotion terminology confusing.

Howard Hinnant wrote:
On Jun 29, 2004, at 12:30 PM, Michael Glassford wrote:
Also, you could end up with some interesting situations like this:
[...] Let's assume that the guarded entity is int x;
void f(read_write_mutex m) { read_write_mutex::read_lock r(m);
int y = x;
if (...) { read_write_mutex::write_lock w(r); //lock promotion
assert( x == y); // always holds, we had a read+ lock all the time
//... } //Point A }
How 'bout:
void f(read_write_mutex& m) { read_write_mutex::read_lock r(m);
int y = x;
if (...) { r.unlock();
Thread switch, write lock, write to x happens here.
read_write_mutex::write_lock w(m); //... m is write locked
assert( x == y); // may fail
w.transfer_to_read_lock(r); } //Point A ... m is read locked }

boost is back up, yea! :-) On Jul 5, 2004, at 4:33 PM, Peter Dimov wrote:
Howard Hinnant wrote:
On Jun 29, 2004, at 12:30 PM, Michael Glassford wrote:
Also, you could end up with some interesting situations like this:
[...]
Let's assume that the guarded entity is
int x;
void f(read_write_mutex m) { read_write_mutex::read_lock r(m);
int y = x;
if (...) { read_write_mutex::write_lock w(r); //lock promotion
assert( x == y); // always holds, we had a read+ lock all the time
I'm not following that x==y always holds. If two threads concurrently hold a read lock, and concurrently try to obtain a write lock, only one thread will get the write lock, the other will block. Assuming the one thread that got the write lock changes x, then the thread that blocked will fire its assert when it finally unblocks and gets the write lock. What am I missing? -Howard

Howard Hinnant wrote:
I'm not following that x==y always holds. If two threads concurrently hold a read lock, and concurrently try to obtain a write lock, only one thread will get the write lock, the other will block. Assuming the one thread that got the write lock changes x, then the thread that blocked will fire its assert when it finally unblocks and gets the write lock.
What am I missing?
As I understand it, if two threads hold a read lock, no thread can obtain a write lock. Write lock == exclusive access. This corresponds to the POSIX memory model where write accesses must be exclusive, but read accesses can be shared with other read accesses. But I may be wrong. ;-)

On Jul 5, 2004, at 5:43 PM, Peter Dimov wrote:
Howard Hinnant wrote:
I'm not following that x==y always holds. If two threads concurrently hold a read lock, and concurrently try to obtain a write lock, only one thread will get the write lock, the other will block. Assuming the one thread that got the write lock changes x, then the thread that blocked will fire its assert when it finally unblocks and gets the write lock.
What am I missing?
As I understand it, if two threads hold a read lock, no thread can obtain a write lock. Write lock == exclusive access. This corresponds to the POSIX memory model where write accesses must be exclusive, but read accesses can be shared with other read accesses. But I may be wrong. ;-)
So in: void f(read_write_mutex m) { read_write_mutex::read_lock r(m); int y = x; if (...) { read_write_mutex::write_lock w(r); //lock promotion what would happen here if two threads entered f and tried to construct w simultaneously? My take, depending upon how the w(r) constructor is implemented, is either: 1. deadlock. 2. one thread blocks and one thread gets the lock. Imho choice 1 is a buggy implementation of the w(r) constructor. And choice 2 results in the assert(x==y) possibly firing. I believe choice 2 is the only reasonable path. And I also believe that the syntax above might lull a code reader into believing that assert(x==y) should always be true. And therefore I think the following (more explicit) syntax is superior: void f(read_write_mutex m) { read_write_mutex::read_lock r(m); int y = x; if (...) { r.unlock(); read_write_mutex::write_lock w(m); This is essentially how the w(r) ctor must be implemented, if it is to be implemented at all. One could try a try_lock on w if the read count is 1, and then fall back on the above if that doesn't work, but I'm unsure if that really provides a benefit. -Howard

Howard Hinnant <hinnant <at> twcny.rr.com> writes:
My take, depending upon how the w(r) constructor is implemented, is either:
1. deadlock. 2. one thread blocks and one thread gets the lock.
Imho choice 1 is a buggy implementation of the w(r) constructor.
Yes, there's no point creating a library facility that assists you in writing code that deadlocks.
And choice 2 results in the assert(x==y) possibly firing.
I think that in the second case, an exception should be thrown. Typically, if you fail to promote a rw lock from read to write, then you will have to repeat the part of the code that was read-protected after the other thread's write lock has been released: bool try_sale(rw_mutex& m) { read_lock rl(m); long balance = get_balance(); long cost = get_total_cost(); if (balance > cost) { write_lock wl(rl); set_balance(balance - cost); // ... return true; } return false } bool do_sale(rw_mutex& m) { bool succeeded = false; while (true) { try { return try_sale(m); catch (rw_promotion_error&) { // Try the whole thing again } } }
I believe choice 2 is the only reasonable path. And I also believe that the syntax above might lull a code reader into believing that assert(x==y) should always be true. And therefore I think the following (more explicit) syntax is superior:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m);
int y = x;
if (...) { r.unlock(); read_write_mutex::write_lock w(m);
This is essentially how the w(r) ctor must be implemented, if it is to be implemented at all. One could try a try_lock on w if the read count is 1, and then fall back on the above if that doesn't work, but I'm unsure if that really provides a benefit.
But this doesn't achieve anything - if you release the read lock, then you don't know that what held true while it was read-locked is still true once you have acquired a write lock. If you choose to have lock promotion (having only lock demotion is a viable choice, except that there will be certain usage patterns that are more efficient with a fail-able promotion), then it must at least preserve known state across promotion. Matt.

Matthew Vogt wrote:
Howard Hinnant <hinnant <at> twcny.rr.com> writes:
My take, depending upon how the w(r) constructor is implemented, is either:
1. deadlock. 2. one thread blocks and one thread gets the lock.
Imho choice 1 is a buggy implementation of the w(r) constructor.
Yes, there's no point creating a library facility that assists you in writing code that deadlocks.
I agree. That's why I've so far only provided the Boost.Threads read/write lock try_promote() and timed_promote() methods (though you can, of course, get the latter to deadlock by using infinite times), and no promote() method. You could make a promote() method that throws if it can't promote, as you suggest below, but I don't like the idea much.
And choice 2 results in the assert(x==y) possibly firing.
I think that in the second case, an exception should be thrown. Typically, if you fail to promote a rw lock from read to write, then you will have to repeat the part of the code that was read-protected after the other thread's write lock has been released:
[snip example]
I believe choice 2 is the only reasonable path. And I also believe that the syntax above might lull a code reader into believing that assert(x==y) should always be true. And therefore I think the following (more explicit) syntax is superior:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m);
int y = x;
if (...) { r.unlock(); read_write_mutex::write_lock w(m);
This is essentially how the w(r) ctor must be implemented, if it is to be implemented at all. One could try a try_lock on w if the read count is 1, and then fall back on the above if that doesn't work, but I'm unsure if that really provides a benefit.
But this doesn't achieve anything - if you release the read lock, then you don't know that what held true while it was read-locked is still true once you have acquired a write lock.
I agree. If releasing and reacquiring is good enough (and it probably is in many or even most cases), then of course you can do that, but it isn't really lock promotion.
If you choose to have lock promotion (having only lock demotion is a viable choice, except that there will be certain usage patterns that are more efficient with a fail-able promotion), then it must at least preserve known state across promotion.
I agree. Mike

Michael Glassford <glassfordm <at> hotmail.com> writes:
I agree. That's why I've so far only provided the Boost.Threads read/write lock try_promote() and timed_promote() methods (though you can, of course, get the latter to deadlock by using infinite times), and no promote() method. You could make a promote() method that throws if it can't promote, as you suggest below, but I don't like the idea much.
Why don't you like the exception? I'll admit immediately to not having given this much thought, but the exception seems appropriate to a coding pattern based on RAII, since it gives simple control flow back to a user-chosen failure handling point, freeing any previously taken locks. ALso, boost.thread already defines the lock_error exception for lock failure. Matt

Matthew Vogt wrote:
Michael Glassford <glassfordm <at> hotmail.com> writes:
I agree. That's why I've so far only provided the Boost.Threads read/write lock try_promote() and timed_promote() methods (though you can, of course, get the latter to deadlock by using infinite times), and no promote() method. You could make a promote() method that throws if it can't promote, as you suggest below, but I don't like the idea much.
Why don't you like the exception? I'll admit immediately to not having given this much thought, but the exception seems appropriate to a coding pattern based on RAII, since it gives simple control flow back to a user-chosen failure handling point, freeing any previously taken locks.
To me, it just feels like the wrong way to be notified that a lock promotion failed so you have to do something else--a try_promote() seems much better for this. However, if people really wanted a throwing promote(), I'd add it.
ALso, boost.thread already defines the lock_error exception for lock failure.
Yes, it does. Currently it's used for cases where you try to perform an operation that is invalid for the current state of the lock (e.g. unlocking a lock that isn't locked and vice versa, the threading API returns an error when you try to lock a mutex, etc.). Since trying to promote a read-lock isn't really invalid, throwing a lock_error would be an extension of its current meaning; but if that were really a problem, a new exception could be defined, of course. Mike

Michael Glassford wrote:
To me, it just feels like the wrong way to be notified that a lock promotion failed so you have to do something else--a try_promote() seems much better for this. However, if people really wanted a throwing promote(), I'd add it.
The point is that the scoped_write_lock wr( rd ); syntax, if supported, should never "succeed" without giving you a write lock. scoped_try_write_lock wr( rd ); can do that, but not scoped_write_lock. That said, can you show an example that demonstrates how a 'try_promote' can be used? I'm curious.

Peter Dimov wrote:
Michael Glassford wrote:
To me, it just feels like the wrong way to be notified that a lock promotion failed so you have to do something else--a try_promote() seems much better for this. However, if people really wanted a throwing promote(), I'd add it.
The point is that the
scoped_write_lock wr( rd );
syntax, if supported, should never "succeed" without giving you a write lock.
OK, that makes sense. I was still thinking in terms of the non-constructor promotion model.
scoped_try_write_lock wr( rd );
can do that, but not scoped_write_lock.
That said, can you show an example that demonstrates how a 'try_promote' can be used? I'm curious.
I was still thinking in terms of the non-constructor promotion model here as well: void f(read_write_mutex& m) { read_write_mutex::try_read_write_lock l(m, read_lock); //do "read stuff" if (!l.try_promote()) { l.unlock(); l.write_lock(); //redo some of "read stuff" } //do write stuff } Translating this to the constructor model of promotion would have to look something like this, I guess: void f(read_write_mutex& m) { read_write_mutex::try_read_lock r(m); //do "read stuff" read_write_mutex::try_write_lock w(r, /*maybe a parameter here to use try_promote() instead of promote()*/); if (!w.locked()) { r.unlock(); w.lock(); //redo some of "read stuff" } //do write stuff }

Michael Glassford <glassfordm <at> hotmail.com> writes:
Translating this to the constructor model of promotion would have to look something like this, I guess:
void f(read_write_mutex& m) { read_write_mutex::try_read_lock r(m);
//do "read stuff"
read_write_mutex::try_write_lock w(r, /*maybe a parameter here to use try_promote() instead of promote()*/);
if (!w.locked()) { r.unlock(); w.lock(); //redo some of "read stuff" }
//do write stuff }
You could encapsulate some of that in the constructor of the write lock: void f(read_write_mutex& m) { read_write_mutex::read_lock r(m); do_read_stuff(); read_write_mutex::write_lock w(r); // Upgrade, releasing and re-acquiring // if necessary if (!w.atomically_promoted()) { do_read_stuff(); } do_write_stuff(); } It certainly looks error-prone, by requiring a test and the repetition of the earlier code. Makes you wonder if it should have a function to execute on non-atomic upgrade: { read_write_mutex::read_lock r(m); do_read_stuff(); read_write_mutex::write_lock w(r, bind(this, &class::do_read_stuff)); do_write_stuff(); } You could reduce further, but I'm sure it will collapse under the weight of real code :) Matt

Matthew Vogt wrote:
Michael Glassford <glassfordm <at> hotmail.com> writes:
Translating this to the constructor model of promotion would have to look something like this, I guess:
void f(read_write_mutex& m) { read_write_mutex::try_read_lock r(m);
//do "read stuff"
read_write_mutex::try_write_lock w(r, /*maybe a parameter here to use try_promote() instead of promote()*/);
if (!w.locked()) { r.unlock(); w.lock(); //redo some of "read stuff" }
//do write stuff }
You could encapsulate some of that in the constructor of the write lock:
void f(read_write_mutex& m) { read_write_mutex::read_lock r(m);
do_read_stuff();
read_write_mutex::write_lock w(r); // Upgrade, releasing and re-acquiring // if necessary if (!w.atomically_promoted()) { do_read_stuff(); }
do_write_stuff(); }
It certainly looks error-prone, by requiring a test and the repetition of the earlier code. Makes you wonder if it should have a function to execute on non-atomic upgrade:
{ read_write_mutex::read_lock r(m);
do_read_stuff();
read_write_mutex::write_lock w(r, bind(this, &class::do_read_stuff));
do_write_stuff(); }
You could reduce further, but I'm sure it will collapse under the weight of real code :)
However, I don't see any way to avoid re-trying after the write-lock is obtained when promotion fails, and I don't see a way to prevent promotion from failing except for the suggested latent-read-lock / upgradeable-read-lock. I agree that such a lock is a good idea, but it does have the drawback of serializing all upgradeable read locks, which could be quite undesirable in some circumstances. Mike

On Jul 7, 2004, at 9:26 AM, Michael Glassford wrote:
However, I don't see any way to avoid re-trying after the write-lock is obtained when promotion fails, and I don't see a way to prevent promotion from failing except for the suggested latent-read-lock / upgradeable-read-lock. I agree that such a lock is a good idea, but it does have the drawback of serializing all upgradeable read locks, which could be quite undesirable in some circumstances.
I've been giving the fail-able read promotion more thought... The purpose of using read/write locks is presumably to allow concurrent reads. In such a design, concurrent reads are probably expected as the norm, else the cheaper and simpler scoped_lock is likely to be more appropriate. Given that concurrent reads are likely to be a normal occurrence, it follows that a failed read_lock promotion would also be a normal occurrence. In the event of a failed read_lock promotion, the typical strategy is to accept that you have to block for write access, and to reread (or recompute) whatever you obtained under the read lock. Given that the expected norm is fail-block-reread, is there any advantage to this approach? That is, in a design where you expect multiple concurrent read promotions, they have to be serialized no matter what. In a design where you expect multiple concurrent read promotions to be rare the upgradable_read_lock, or even the far simpler scoped_lock (in the case concurrent reads are rare) should not incur a performance hit. The big motivation that I see for the upgradable_read_lock is the scenario linked to by Alexander yesterday: On Jul 6, 2004, at 5:02 AM, Alexander Terekhov wrote:
Read this thread:
http://groups.google.com/groups?threadm=ijnd7a.3pb.ln%40tux.localdomain (Subject: upgradeable lock)
If you have multiple long read/compute then write threads, there is no other logic that solves the problem other than serialization, no matter how it is coded. The paragraph that David Butenhof wrote in response to the fail-able read promotion design (which Alexander also quoted here) summed up (and cemented) my thoughts as well. -Howard

Howard Hinnant <hinnant@twcny.rr.com> writes:
On Jul 7, 2004, at 9:26 AM, Michael Glassford wrote:
However, I don't see any way to avoid re-trying after the write-lock is obtained when promotion fails, and I don't see a way to prevent promotion from failing except for the suggested latent-read-lock / upgradeable-read-lock. I agree that such a lock is a good idea, but it does have the drawback of serializing all upgradeable read locks, which could be quite undesirable in some circumstances.
I've been giving the fail-able read promotion more thought...
The purpose of using read/write locks is presumably to allow concurrent reads. In such a design, concurrent reads are probably expected as the norm, else the cheaper and simpler scoped_lock is likely to be more appropriate. Given that concurrent reads are likely to be a normal occurrence, it follows that a failed read_lock promotion would also be a normal occurrence. In the event of a failed read_lock promotion, the typical strategy is to accept that you have to block for write access, and to reread (or recompute) whatever you obtained under the read lock.
If that's so, I'd have to say that an exception is a very poor choice for reporting failure to acquire the lock. I don't know whether that's still under consideration... -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

Howard Hinnant wrote:
On Jul 7, 2004, at 9:26 AM, Michael Glassford wrote:
However, I don't see any way to avoid re-trying after the write-lock is obtained when promotion fails, and I don't see a way to prevent promotion from failing except for the suggested latent-read-lock / upgradeable-read-lock. I agree that such a lock is a good idea, but it does have the drawback of serializing all upgradeable read locks, which could be quite undesirable in some circumstances.
I've been giving the fail-able read promotion more thought...
The purpose of using read/write locks is presumably to allow concurrent reads. In such a design, concurrent reads are probably expected as the norm, else the cheaper and simpler scoped_lock is likely to be more appropriate. Given that concurrent reads are likely to be a normal occurrence, it follows that a failed read_lock promotion would also be a normal occurrence.
It would be a normal occurrence, but not necessarily because of concurrent reads. It would be easy for promotion from read-lock to write-lock to a) prevent any new read or write locks, b) wait until all current read-locks are released, and then c) obtain the write lock. The only time a promotion request needs to fail is when multiple threads request promotion simultaneously; all but one must fail and relinquish its read lock to prevent deadlock. Other than that, I agree with everything you've said. (Actually, re-reading what you've said below, I think maybe that's actually what you meant anyway).
In the event of a failed read_lock promotion, the typical strategy is to accept that you have to block for write access, and to reread (or recompute) whatever you obtained under the read lock.
Given that the expected norm is fail-block-reread, is there any advantage to this approach? That is, in a design where you expect multiple concurrent read promotions, they have to be serialized no matter what.
True. The only advantage I can see is in cases where you have many readers that have to read in order to see if they need write access, and only few of them ever actually need it. The many checks can happen simultaneously, and the time saved by the concurrency outweighs the cost of the few re-reads. Having said that, I'm not at all tied to that type of scheme. I've thought all along that lock demotion is more important than lock promotion (at least for my typical uses of a read/write lock).
In a design where you expect multiple concurrent read promotions to be rare the upgradable_read_lock, or even the far simpler scoped_lock (in the case concurrent reads are rare) should not incur a performance hit.
The big motivation that I see for the upgradable_read_lock is the scenario linked to by Alexander yesterday:
On Jul 6, 2004, at 5:02 AM, Alexander Terekhov wrote:
Read this thread:
http://groups.google.com/groups?threadm=ijnd7a.3pb.ln%40tux.localdomain (Subject: upgradeable lock)
If you have multiple long read/compute then write threads, there is no other logic that solves the problem other than serialization, no matter how it is coded.
The paragraph that David Butenhof wrote in response to the fail-able read promotion design (which Alexander also quoted here) summed up (and cemented) my thoughts as well.
All this makes sense to me. Again, I like the upgradeable lock agree that the that it's cleaner and easier to use. Mike

On Wed, 7 Jul 2004 11:10:22 -0400, Howard Hinnant <hinnant@twcny.rr.com> wrote:
I've been giving the fail-able read promotion more thought...
The purpose of using read/write locks is presumably to allow concurrent reads. In such a design, concurrent reads are probably expected as the norm, else the cheaper and simpler scoped_lock is likely to be more appropriate. Given that concurrent reads are likely to be a normal occurrence, it follows that a failed read_lock promotion would also be a normal occurrence. In the event of a failed read_lock promotion, the typical strategy is to accept that you have to block for write access, and to reread (or recompute) whatever you obtained under the read lock.
I think it is better to see this third kind of lock as a write lock instead of as a read lock. IMHO, it doesn't follow from concurrent reads being a normal occurrence that a failed read_lock promotion would also be a normal occurrence. If promotion isn't available in any way, you wouldn't have a choice but to use write_lock. A write_lock blocks for two reasons: 1) it has to wait it's turn to be the only writer, and 2) it has to wait until no more readers remain. A latent_write_lock gives you the oportunity to do work while only 1) has been achieved; later you can apply for 2). Seeing it this way, there's a progression in optimization from ScopedLock, read_lock - write_lock and read_lock - latent_write_lock - write_lock. Note I think failable promotion could be useful even having latent_write_lock. Bruno Martinez

What about a latent_write_lock? It's semantics would be to block on contruction until it only shares the mutex with read_locks. A write_lock could be contructed from a latent_write_lock and this would always work, because another latent_write_lock would still be blocked in it's constructor. This kind of lock would be useful for situations in which you have to read a structure to know if you have to write on it. Bruno Martinez

Bruno Martínez Aguerre <br1 <at> internet.com.uy> writes:
What about a latent_write_lock?
It's semantics would be to block on contruction until it only shares the mutex with read_locks. A write_lock could be contructed from a latent_write_lock and this would always work, because another latent_write_lock would still be blocked in it's constructor. This kind of lock would be useful for situations in which you have to read a structure to know if you have to write on it.
Bruno Martinez
Interesting, this divides read locks into promotable and non-promotable variants? If you have a design where a lot of threads take concurrent read locks, and the case of promotion is rare, then having them all use latent_write_locks will unnecessarily serialise them. Allowing promotion failure instead would only affect rare cases of promotion contention. If you have a case where promotion is common, then you probably won't gain much over simply using write locks, will you? If you can mix a number of read-only locks with a small number of latent_write_locks for which promotion is uncommon, then you have an optimum result. Matt

On Tue, 6 Jul 2004 03:29:15 +0000 (UTC), Matthew Vogt <mvogt@juptech.com> wrote:
Bruno Martínez Aguerre <br1 <at> internet.com.uy> writes:
What about a latent_write_lock?
It's semantics would be to block on construction until it only shares the mutex with read_locks. A write_lock could be constructed from a latent_write_lock and this would always work, because another latent_write_lock would still be blocked in it's constructor. This kind of lock would be useful for situations in which you have to read a structure to know if you have to write on it.
Interesting, this divides read locks into promotable and non-promotable variants?
Yes.
If you have a design where a lot of threads take concurrent read locks, and the case of promotion is rare, then having them all use latent_write_locks will unnecessarily serialise them. Allowing promotion failure instead would only affect rare cases of promotion contention.
That's true. If promotion is uncommon, the best solution would be to unlock the read_lock and construct a normal write_lock. It's a rare situation, and the code is simpler this way than dealing with exceptions/retries.
If you have a case where promotion is common, then you probably won't gain much over simply using write locks, will you?
If promotion is common and the reading part is long, a latent_write_lock would be better than a write_lock because it wouldn't block normal readers until necesary. Trying to promote a read_lock and failing would be more expensive because the reading would have to be done again.
If you can mix a number of read-only locks with a small number of latent_write_locks for which promotion is uncommon, then you have an optimum result.
I'll have to think about this last sentence a little more. Bruno Martinez

Bruno Martínez Aguerre wrote:
On Tue, 6 Jul 2004 03:29:15 +0000 (UTC), Matthew Vogt <mvogt@juptech.com> wrote:
Bruno Martínez Aguerre <br1 <at> internet.com.uy> writes:
What about a latent_write_lock?
It's semantics would be to block on construction until it only shares the mutex with read_locks. A write_lock could be constructed from a latent_write_lock and this would always work, because another latent_write_lock would still be blocked in it's constructor. This kind of lock would be useful for situations in which you have to read a structure to know if you have to write on it.
Interesting,
I think so, too. Given the current implementation of Boost.Threads read/write locks, it wouldn't be too difficult to add this, if it was determined to be worthwhile. But I wonder how easy it would be to add to other implementations? After all, Boost is only partly about providing an implementation, but even more about specifying a well-designed, widely-implementable interface.
this divides read locks into promotable and non-promotable variants? Yes.
Or perhaps into promotable-without-fail and promotable-with-fail variants.
If you have a design where a lot of threads take concurrent read locks, and the case of promotion is rare, then having them all use latent_write_locks will unnecessarily serialise them. Allowing promotion failure instead would only affect rare cases of promotion contention.
That's true. If promotion is uncommon, the best solution would be to unlock the read_lock and construct a normal write_lock. It's a rare situation, and the code is simpler this way than dealing with exceptions/retries.
If you have a case where promotion is common, then you probably won't gain much over simply using write locks, will you?
If promotion is common and the reading part is long, a latent_write_lock would be better than a write_lock because it wouldn't block normal readers until necesary.
Two comments: the more common promotion is, the fewer normal readers there will be; and, since all latent readers are serialized, it only makes sense to use latent readers if a high percentage of them will end up being promoted, even if reading is expensive (in other words, it's a tradeoff between how expensive reading is and how expensive serialization is).
Trying to promote a read_lock and failing would be more expensive because the reading would have to be done again.
If you can mix a number of read-only locks with a small number of latent_write_locks for which promotion is uncommon, then you have an optimum result.
I'll have to think about this last sentence a little more.
Mike

I like the latent_write_lock / upgradeable lock idea proposed by Bruno / Alexander. I have been playing with the following interface: namespace detail { template <class RW_Mutex> class read_lock { public: typedef RW_Mutex mutex_type; explicit read_lock(mutex_type& m, bool lock_it = true); explicit read_lock(upgradable_read_lock<mutex_type>& r); explicit read_lock(write_lock<mutex_type>& w); ~read_lock(); void lock(); bool try_lock(); void unlock(); bool locked() const; operator int bool_type::* () const; private: read_lock(const read_lock&); read_lock& operator=(const read_lock&); }; template <class RW_Mutex> class upgradable_read_lock { public: typedef RW_Mutex mutex_type; explicit upgradable_read_lock(mutex_type& m, bool lock_it = true); explicit upgradable_read_lock(write_lock<mutex_type>& w); ~upgradable_read_lock(); void lock(); bool try_lock(); void unlock(); bool locked() const; operator int bool_type::* () const; void transfer_to(read_lock<mutex_type>& r); void transfer_to(write_lock<mutex_type>& w); private: upgradable_read_lock(const upgradable_read_lock&); upgradable_read_lock& operator=(const upgradable_read_lock&); }; template <class RW_Mutex> class write_lock { public: typedef RW_Mutex mutex_type; explicit write_lock(mutex_type& m, bool lock_it = true); explicit write_lock(upgradable_read_lock<mutex_type>& r); ~write_lock(); void lock(); bool try_lock(); void unlock(); bool locked() const; operator int bool_type::* () const; void transfer_to(read_lock<mutex_type>& r); void transfer_to(upgradable_read_lock<mutex_type>& r); private: write_lock(const write_lock&); write_lock& operator=(const write_lock&); }; } // detail class rw_mutex { public: typedef detail::read_lock<rw_mutex> read_lock; typedef detail::upgradable_read_lock<rw_mutex> upgradable_read_lock; typedef detail::write_lock<rw_mutex> write_lock; rw_mutex(); ~rw_mutex(); private: rw_mutex(const rw_mutex&); rw_mutex& operator=(const rw_mutex&); }; There are 3 types of locks: read_lock, upgradable_read_lock and write_lock. Conversions (transfer of mutex ownership) between these types will only occur in an atomic fashion (blocking when appropriate). Conversions that might result in a non-atomic operation are not allowed. Conversions take two forms: 1. Conversion constructor. 2. Member function: void transfer_to(...); The conversion ctor locks only if its argument was locked. The ctor argument is unlocked after the construction. The transfer_to function will throw a lock_error if the lock was not locked, or if the argument does not refer to the same underlying mutex. When s.transfer_to(d) returns (normally), s is unlocked and d is locked (atomically). Destructors unlock only themselves, and have no effect on other locks (there is no persistent linking implied by mutex ownership transfer). If such a link is desired, it is easily built on top of this framework. As described by others, read_locks can share ownership among themselves and not more than one upgradable_read_lock. write_locks maintain exclusive ownership. Here is how some recent code examples might look: void f(rw_mutex& m) { rw_mutex::upgradable_read_lock r(m); if (...) { rw_mutex::write_lock w(r); //lock promotion //... w.transfer_to(r); } //Point A } If you wanted to catch exceptions at Point A thrown while writing, and still have the read_lock, a simple auxiliary RAII class could be employed: template <class Lock1, class Lock2> class helper { public: explicit helper(Lock2& l2) : l1_(l2), l2_(l2) {} ~helper() {l1_.transfer_to(l2_);} private: helper(const helper&); helper& operator=(const helper&); Lock1 l1_; Lock2& l2_; }; and then f would look like: void f(rw_mutex& m) { rw_mutex::upgradable_read_lock r(m); if (...) { helper<rw_mutex::write_lock, rw_mutex::upgradable_read_lock> w(r); //... } //Point A } Note that if you don't catch exceptions at Point A, the helper class is needless complexity and transfer of the mutex. It would also be a needless transfer if there is no code at Point A, or if the write section returns early. That is why I like the locks to be unlinked. Linking them would add space overhead to the locks, and time overhead to the destructors, when it is not always desired. But linking is easily built on top with a helper class as shown (only pay for what you use). Another example: void f(rw_mutex& m) { rw_mutex::write_lock w(m); if (...) { rw_mutex::upgradable_read_lock r(w); //lock demotion //... r.transfer_to(w); } //Point A } Same notes about exceptions and helpers. If one accidently codes read_lock instead of upgradable_read_lock, then the transfer_to() call will fail at compile time. If the r.tranfer_to(w) is not needed however, then a read_lock will work just fine. The downside to all this is that the sizeof(rw_mutex) about doubled to support upgradable_read_lock (whether or not you use that functionality). The code size of the read and write locking/unlocking also swelled somewhat. I'm still contemplating the cost/functionality tradeoff, but my current feeling is that it is probably worth it. -Howard

Howard Hinnant wrote:
I like the latent_write_lock / upgradeable lock idea proposed by Bruno / Alexander. I have been playing with the following interface:
namespace detail {
template <class RW_Mutex> class read_lock { public: typedef RW_Mutex mutex_type;
explicit read_lock(mutex_type& m, bool lock_it = true); explicit read_lock(upgradable_read_lock<mutex_type>& r); explicit read_lock(write_lock<mutex_type>& w);
~read_lock();
void lock(); bool try_lock(); void unlock(); bool locked() const; operator int bool_type::* () const;
private: read_lock(const read_lock&); read_lock& operator=(const read_lock&); };
[...] Seems very good to me. Also demonstrates a better lock taxonomy (which allows me to include some on-topic content ;-) ): Lock Lock( Mutex&, bool = true ); void lock(); void unlock(); bool locked() const; operator int bool_type::* () const; TryLock: Lock +bool try_lock(); TimedLock: TryLock +bool timed_lock( xtime const & );

Peter Dimov wrote:
Howard Hinnant wrote:
I like the latent_write_lock / upgradeable lock idea proposed by Bruno / Alexander. I have been playing with the following interface:
namespace detail {
template <class RW_Mutex> class read_lock { public: typedef RW_Mutex mutex_type;
explicit read_lock(mutex_type& m, bool lock_it = true); explicit read_lock(upgradable_read_lock<mutex_type>& r); explicit read_lock(write_lock<mutex_type>& w);
~read_lock();
void lock(); bool try_lock(); void unlock(); bool locked() const; operator int bool_type::* () const;
private: read_lock(const read_lock&); read_lock& operator=(const read_lock&); };
[...]
Seems very good to me. Also demonstrates a better lock taxonomy
Better compared to what? The current read/write lock taxonomy? One of the many suggestions in this discussion?
(which allows me to include some on-topic content ;-) ):
Lock
Lock( Mutex&, bool = true ); void lock(); void unlock(); bool locked() const; operator int bool_type::* () const;
TryLock: Lock
+bool try_lock();
TimedLock: TryLock
+bool timed_lock( xtime const & );
The point being that TryLock is a refinement of Lock, and TimedLock is a refinement of TryLock? If so, I agree that's better than TryLock and TimedLock both refining Lock. There are still the constructor issues to deal with in such a lock taxonomy, however. The main one: how do you define destructors that are consistent within a lock concept and also consistent between concepts. E.g., on the one hand it seems that a one-parameter constructor should do the same thing in all lock types--so it would have to block; on the other hand, it seems that TryLock constructors should not block unless instructed to do otherwise. Mike

Michael Glassford wrote:
Peter Dimov wrote:
Howard Hinnant wrote:
[ read/write mutexes and locks ]
Seems very good to me. Also demonstrates a better lock taxonomy
Better compared to what? The current read/write lock taxonomy? One of the many suggestions in this discussion?
Better compared to the current non-read-write lock taxonomy.
Lock
Lock( Mutex&, bool = true ); void lock(); void unlock(); bool locked() const; operator int bool_type::* () const;
TryLock: Lock
+bool try_lock();
TimedLock: TryLock
+bool timed_lock( xtime const & );
The point being that TryLock is a refinement of Lock, and TimedLock is a refinement of TryLock? If so, I agree that's better than TryLock and TimedLock both refining Lock. There are still the constructor issues to deal with in such a lock taxonomy, however.
I don't see any. There is one constructor, with the same behavior.
The main one: how do you define destructors that are consistent within a lock concept and also consistent between concepts.
~Lock { if( locked() ) unlock(); } however it looks like you meant 'constructor'.
E.g., on the one hand it seems that a one-parameter constructor should do the same thing in all lock types--so it would have to block; on the other hand, it seems that TryLock constructors should not block unless instructed to do otherwise.
I meant what I said. There is a single constructor. Its behavior is consistent across all lock concepts (it it weren't, a TryLock would not be a Lock). A mutex exposes a single lock type, which is as refined as possible.

Peter Dimov wrote:
Michael Glassford wrote:
Peter Dimov wrote:
Howard Hinnant wrote:
[ read/write mutexes and locks ]
Seems very good to me. Also demonstrates a better lock taxonomy
Better compared to what? The current read/write lock taxonomy? One of the many suggestions in this discussion?
Better compared to the current non-read-write lock taxonomy.
Lock
Lock( Mutex&, bool = true ); void lock(); void unlock(); bool locked() const; operator int bool_type::* () const;
TryLock: Lock
+bool try_lock();
TimedLock: TryLock
+bool timed_lock( xtime const & );
The point being that TryLock is a refinement of Lock, and TimedLock is a refinement of TryLock? If so, I agree that's better than TryLock and TimedLock both refining Lock. There are still the constructor issues to deal with in such a lock taxonomy, however.
I don't see any. There is one constructor, with the same behavior.
OK. I hadn't realized that you meant there to be literally only one constructor; I do now.
The main one: how do you define destructors that are consistent within a lock concept and also consistent between concepts.
~Lock { if( locked() ) unlock(); }
however it looks like you meant 'constructor'.
Sorry, I did mean constructor.
E.g., on the one hand it seems that a one-parameter constructor should do the same thing in all lock types--so it would have to block; on the other hand, it seems that TryLock constructors should not block unless instructed to do otherwise.
I meant what I said. There is a single constructor. Its behavior is consistent across all lock concepts (it it weren't, a TryLock would not be a Lock). A mutex exposes a single lock type, which is as refined as possible.
OK, that makes sense and is consistent (which I like). What about the following as an alternative? I retain the hierarchy (a TryLock is a Lock, a TimedLock is a TryLock) and resolve the conflict (of consistency within a concept and consistency across concepts) by the principle that a constructor always blocks unless you specifically tell it not to: Definitions ----------- M: appropriate mutex type namespace lock_state { typedef enum { unlocked=0, locked=1 } lock_state; } //namespace lock_state namespace read_write_lock_state { typedef enum { unlocked=0, read_locked=1, write_locked=2 } read_write_lock_state; } //namespace read_write_lock_state namespace blocking_mode { typedef enum { non_blocking=0, blocking=1 } blocking_mode; } //namespace blocking_mode ScopedLock / ScopedReadLock / ScopedWriteLock --------------------------------------------- lock(M) //always locking, blocking lock(M, lock_state) //blocking ScopedTryLock / ScopedTryReadLock / ScopedTryWriteLock ------------------------------------------------------ try_lock(M) //always locking, BLOCKING try_lock(M, lock_state) //BLOCKING try_lock(M, lock_state, blocking_mode) try_lock(M, blocking_mode) //always locking ScopedTimedLock / ScopedTimedReadLock / ScopedTimedWriteLock ------------------------------------------------------------ timed_lock(M) //always locking, blocking timed_lock(M, lock_state) //blocking timed_lock(M, lock_state, blocking_mode) timed_lock(M, blocking_mode) //always locking timed_lock(M, t) //always locking, blocking for time t

Michael Glassford wrote:
What about the following as an alternative? I retain the hierarchy (a TryLock is a Lock, a TimedLock is a TryLock) and resolve the conflict
(of
consistency within a concept and consistency across concepts) by the principle that a constructor always blocks unless you specifically tell it not to:
Definitions ----------- M: appropriate mutex type
namespace lock_state { typedef enum { unlocked=0, locked=1 } lock_state; } //namespace lock_state
namespace read_write_lock_state { typedef enum { unlocked=0, read_locked=1, write_locked=2 } read_write_lock_state; } //namespace read_write_lock_state
namespace blocking_mode { typedef enum { non_blocking=0, blocking=1 } blocking_mode; } //namespace blocking_mode
ScopedLock / ScopedReadLock / ScopedWriteLock --------------------------------------------- lock(M) //always locking, blocking lock(M, lock_state) //blocking
ScopedTryLock / ScopedTryReadLock / ScopedTryWriteLock ------------------------------------------------------ try_lock(M) //always locking, BLOCKING try_lock(M, lock_state) //BLOCKING
try_lock(M, lock_state, blocking_mode) try_lock(M, blocking_mode) //always locking
ScopedTimedLock / ScopedTimedReadLock / ScopedTimedWriteLock ------------------------------------------------------------ timed_lock(M) //always locking, blocking timed_lock(M, lock_state) //blocking
timed_lock(M, lock_state, blocking_mode) timed_lock(M, blocking_mode) //always locking
timed_lock(M, t) //always locking, blocking for time t
This is certainly possible, but I don't see what the additional complexity buys us. TryLock l( m, false ); if( l.try_lock() ) { } looks acceptable to me. TryLock l( m, non_blocking ); if( l.locked() ) { } doesn't seem much of an improvement.

"Peter Dimov" <pdimov@mmltd.net> writes:
This is certainly possible, but I don't see what the additional complexity buys us.
TryLock l( m, false );
if( l.try_lock() ) { }
looks acceptable to me.
TryLock l( m, non_blocking );
if( l.locked() ) { }
doesn't seem much of an improvement.
It does to me. I like names that say what they mean; false could easily be misinterpreted. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote:
"Peter Dimov" <pdimov@mmltd.net> writes:
This is certainly possible, but I don't see what the additional complexity buys us.
TryLock l( m, false );
if( l.try_lock() ) { }
looks acceptable to me.
TryLock l( m, non_blocking );
if( l.locked() ) { }
doesn't seem much of an improvement.
It does to me. I like names that say what they mean; false could easily be misinterpreted.
'false' is existing practice in this case, which is why I used it in the example. But it's a distraction; to level the playing field compare: TryLock l( m, unlocked ); if( l.try_lock() ) { } with: TryLock l( m, non_blocking ); if( l.locked() ) { }

Peter Dimov wrote:
compare:
TryLock l( m, unlocked );
if( l.try_lock() ) { }
with:
TryLock l( m, non_blocking );
if( l.locked() ) { }
(Jumping late, forgive me if this has been discussed already.) How about: if( ScopedLock l = try_lock( m ) ) { } where try_lock is function that returns a simple wrapper class: template< Mutex > struct try_locker { Mutex & m; ... }; template< Mutex > try_locker< Mutex > try_lock( Mutex & m ) { return try_locker< Mutex >( m ); }; and ScopedLock is some lockable type which has a constructor that accepts a try_locker. (It would also need a bool-ish conversion to allow the lock to be declared in the "if" statement, but that's not relevant to this discussion.) -- Eric Niebler Boost Consulting www.boost-consulting.com

Eric Niebler wrote:
if( ScopedLock l = try_lock( m ) ) { }
where try_lock is function that returns a simple wrapper class:
I prefer ScopedGuard-like implementation because it doesn't introduce neither new try_lock function nor its return type (return type is somewhat similar to lock class for new users yet is very different from the lock because, for example, it's CopyConstructible => source of confusion). If the code above were based on that idiom, try_lock/timed_lock/whatever_lock classes could be "reused". On the one hand, they could be used for standalone guards, on the other hand, it could be possible to compile your code. Of course, ScopedLock has to be const reference to some base class of all lock classes. http://lists.boost.org/MailArchives/boost/msg49847.php -- Alexander Nasonov

Eric Niebler wrote:
Peter Dimov wrote:
compare:
TryLock l( m, unlocked );
if( l.try_lock() ) { }
with:
TryLock l( m, non_blocking );
if( l.locked() ) { }
(Jumping late, forgive me if this has been discussed already.)
How about:
if( ScopedLock l = try_lock( m ) ) { }
where try_lock is function that returns a simple wrapper class:
template< Mutex > struct try_locker { Mutex & m; ... };
template< Mutex > try_locker< Mutex > try_lock( Mutex & m ) { return try_locker< Mutex >( m ); };
and ScopedLock is some lockable type which has a constructor that accepts a try_locker. (It would also need a bool-ish conversion to allow the lock to be declared in the "if" statement, but that's not relevant to this discussion.)
Interesting idea. I suppose you could even do this: if (ScopedLock l(m, locker())) { } where locker() is a function object that defines operator()(LockType& l) which the lock calls in the constructor, passing *this. You could have locker(), try_locker(), timed_locker(t), etc. Mike

Michael Glassford wrote:
Eric Niebler wrote:
How about:
if( ScopedLock l = try_lock( m ) ) { }
where try_lock is function that returns a simple wrapper class:
template< Mutex > struct try_locker { Mutex & m; ... };
template< Mutex > try_locker< Mutex > try_lock( Mutex & m ) { return try_locker< Mutex >( m ); };
and ScopedLock is some lockable type which has a constructor that accepts a try_locker. (It would also need a bool-ish conversion to allow the lock to be declared in the "if" statement, but that's not relevant to this discussion.)
Interesting idea. I suppose you could even do this:
if (ScopedLock l(m, locker())) { }
where locker() is a function object that defines
operator()(LockType& l)
which the lock calls in the constructor, passing *this. You could have locker(), try_locker(), timed_locker(t), etc.
Not quite. In order to declare and initialize a variable in an "if" statement, you need to use the "if( type var = fun() )" form, where the "=" is necessary. I realize that the code I posted before is incompatible with the requirement that ScopedLock be non-copyable. Below is an interesting variation which works around the problem. It defines a single type "lock" which is non-copyable, yet it can be returned by value from a function. (This may or may not be desirable for a lock class, but it's interesting.) struct lock; lock try_lock(); struct lock_ref { lock_ref(lock & ref) : ref_(ref) { } lock & ref_; }; struct lock { lock(lock_ref ref){} operator lock_ref() { // ... move the lock ... return lock_ref(*this); } operator bool() const { // TODO return true iff we're holding the lock return true; } private: friend lock try_lock(); lock(/*params*/){} // a try-lock c'tor // make this type non-copyable, non-assignable lock(lock &); lock & operator=(lock &); }; inline lock try_lock(/*params*/) { // call a special try-lock c'tor on lock return lock(/*params*/); } int main() { if( lock l = try_lock(/*params*/) ) {} } The idea is to have just one lock class (leaving aside read/write locks) and a collection of factory functions like try_lock(), timed_lock(), defered_lock() etc. Those functions create a lock object by calling the appropriate (private) constructor and returning the new lock object. (Some fancy auto_ptr-like move shenanigans are needed to make the lock return-able but not copy-able.) I'm not really advocating this design. I'm not close enough to this problem to have a strong preference. Just offering it as an alternative. -- Eric Niebler Boost Consulting www.boost-consulting.com

Eric Niebler wrote:
Michael Glassford wrote:
Eric Niebler wrote:
[snip]
Interesting idea. I suppose you could even do this:
if (ScopedLock l(m, locker())) { }
where locker() is a function object that defines
operator()(LockType& l)
which the lock calls in the constructor, passing *this. You could have locker(), try_locker(), timed_locker(t), etc.
Not quite. In order to declare and initialize a variable in an "if" statement, you need to use the "if( type var = fun() )" form, where the "=" is necessary.
Sorry, it was too late at night and I missed that. I suggest an alternate implementation in another message. Mike

Eric Niebler wrote:
How about:
if( ScopedLock l = try_lock( m ) ) { }
You know that I very much favor declarations in conditions, but this is a whole new design, one that uses moveable locks. Something like: class Mutex { public: AutoLock lock(); AutoLock try_lock(); AutoLock timed_lock( xtime ); }; One might argue that a lock is naturally moveable... but I think that we should constrain ourselves to (fixing) the current noncopyable scoped_lock idiom.

On Jul 8, 2004, at 7:56 AM, Peter Dimov wrote:
One might argue that a lock is naturally moveable...
It is.
but I think that we should constrain ourselves to (fixing) the current noncopyable scoped_lock idiom.
Agreed. I don't particularly want to tackle truly moving locks until I have an rvalue-reference to do it with. The auto_ptr-like hacks are downright demotivating. ;-( -Howard

Peter Dimov wrote:
Eric Niebler wrote:
How about:
if( ScopedLock l = try_lock( m ) ) { }
You know that I very much favor declarations in conditions, but this is a whole new design, one that uses moveable locks. Something like:
class Mutex { public:
AutoLock lock(); AutoLock try_lock(); AutoLock timed_lock( xtime ); };
One might argue that a lock is naturally moveable...
This does need to be addressed someday, but...
but I think that we should constrain ourselves to (fixing) the current noncopyable scoped_lock idiom.
I agree that movable locks should wait. However, what about something like this, which doesn't require movable locks (concept only--there are probably errors): //----- locker classes -----/ template <typename Mutex> class unlocked { public: unlocked(Mutex& m) : m_(m) {} template <typename Lock> operator () (Lock& l) const {} }; template <typename Mutex> class locked { public: locked(Mutex& m) : m_(m) {} template <typename Lock> operator () (Lock& l) const {l.lock();} }; template <typename Mutex> class try_locked { public: try_locked(Mutex& m) : m_m(m) {} template <typename Lock> operator () (Lock& l) const {l.try_lock();} Mutex m_; private: bool locked_; }; template <typename Mutex> class timed_locked { public: timed_locked(Mutex& m, const xtime& t) : m_m(m), t_(t) {} template <typename Lock> operator () (Lock& l) const {l.timed_lock(t);} Mutex m_; private: bool t_; }; //----- lock classes -----/ class lock { public: template <class Locker> try_lock(const Locker& locker) : m_(locker.m_) {locker(*this);} } class try_lock { public: template <class Locker> try_lock(const Locker& locker) : m_(locker.m_) {locker(*this);} } class timed_lock { public: template <class Locker> try_lock(const Locker& locker) : m_(locker.m_) {locker(*this);} } //----- example -----/ mutex m; lock l1 = unlocked(m); lock l2 = locked(m); try_lock l3 = unlocked(m); try_lock l4 = locked(m); if (try_lock l5 = try_locked(m)) {/*...*/} timed_lock l6 = unlocked(m); timed_lock l7 = locked(m); if (timed_lock l8 = try_locked(m)) {/*...*/} if (timed_lock l9 = timed_locked(m, get_xtime()) {/*...*/} To select whether a lock should be locked or not at runtime, you could add constructors to the locker classes that take a bool or enum and redefine their operator() to lock if the constructor parameter indicated that it should. Mike

Michael Glassford wrote:
I agree that movable locks should wait. However, what about something like this, which doesn't require movable locks (concept only--there are probably errors):
[...] Looks like Comeau in strict mode would still not accept it; noncopyable objects cannot be copy-initialized. But let's get back to my original question:
This is certainly possible, but I don't see what the additional complexity buys us.

Peter Dimov wrote:
Michael Glassford wrote:
I agree that movable locks should wait. However, what about something like this, which doesn't require movable locks (concept only--there are probably errors):
[...]
Looks like Comeau in strict mode would still not accept it; noncopyable objects cannot be copy-initialized. But let's get back to my original question:
See the code I posted with my movable lock. It is noncopyable but you can return it from a function and use copy-initialization so you can declare your locks in an "if" statement. It compiles with Comeau strict.
One might argue that a lock is naturally moveable... but I think that we should constrain ourselves to (fixing) the current noncopyable scoped_lock idiom.
Should we consider alternate designs only after the fixed scoped_lock has achieved wide acceptance? After it has been proposed for standardization? After it has been accepted? If you're talking about making breaking changes to the threading library (aren't you?), isn't the time to consider alternate designs now? -- Eric Niebler Boost Consulting www.boost-consulting.com

Eric Niebler wrote:
Should we consider alternate designs only after the fixed scoped_lock has achieved wide acceptance? After it has been proposed for standardization? After it has been accepted? If you're talking about making breaking changes to the threading library (aren't you?), isn't the time to consider alternate designs now?
We _are_ considering alternate designs now. As part of this effort I asked:
This is certainly possible, but I don't see what the additional complexity buys us.

Peter Dimov wrote:
We _are_ considering alternate designs now. As part of this effort I asked:
This is certainly possible, but I don't see what the additional complexity buys us.
My apologies. You had directed that statement at one of Mike's designs, so I didn't think it was directed at mine as well. Let's see ... 1) a reduction in interface complexity. - one lock class, instead of a scoped_lock, try_lock, timed_lock, etc. - no bools or enums in constructors; instead, there are clearly named factory functions for customizing lock construction. 2) You can initialize a lock in the condition of an "if" statement. 3) You can return a lock from a function. (Benefit is debatable.) That's it, I think. -- Eric Niebler Boost Consulting www.boost-consulting.com

Eric Niebler wrote:
1) a reduction in interface complexity. - one lock class, instead of a scoped_lock, try_lock, timed_lock, etc. - no bools or enums in constructors; instead, there are clearly named factory functions for customizing lock construction.
2) You can initialize a lock in the condition of an "if" statement.
3) You can return a lock from a function. (Benefit is debatable.)
After giving it some thought: - one lock class: tie; - no bools or enums in constructors: there is a bool argument, but it's (in my opinion) acceptable since there is a single constructor, and the argument participates directly in its postcondition: Lock( Mutex & m, bool l = true ); Post: locked() == l; - you can return a lock from a function: we can make the locks movable regardless of the interface, so it seems possible to ignore this bullet; - if( lock l = try_lock( m ) ) { } This is the main difference. Declarations in conditions are a nice idiom. The typical use is if( X x = f() ) { // do something with x } Our example doesn't fit the pattern, however, as the "// do something" part typically does not reference the lock itself. I don't have much experience with try locks, though. Pretty much the only example that comes to mind is Howard's lock_both: for(;;) { l1.lock(); if( l2.try_lock() ) return; l1.unlock(); l2.lock(); if( l1.try_lock() ) return; l2.unlock(); } (I hope I got that right) and I don't see how it can be improved by the if() idiom. But I may be missing some important use cases.

Peter Dimov wrote:
- you can return a lock from a function: we can make the locks movable regardless of the interface, so it seems possible to ignore this bullet;
- if( lock l = try_lock( m ) ) { }
This is the main difference.
Declarations in conditions are a nice idiom. The typical use is
if( X x = f() ) { // do something with x }
Our example doesn't fit the pattern, however, as the "// do something" part typically does not reference the lock itself.
This is a degenerate case of more general idea of passing lock reference to functions inside condition: if(lock_ref l = scoped_lock(m)) { // do something with l: action1(l); action2(l); } Apparently, action1 and action2 can't be called without prior obtaining of a lock. Plus, you get some sort of polymorphism because the functions can accept other locks as well: action1(scoped_try_lock(m1)); // lock_ref is const reference action2(scoped_timed_lock(m2)); The only big problem I know of so far is unspecified order of calls in the case of getting multiple locks (incorrect order leads to deadlock): action3(scoped_lock(m1), scoped_lock(m2)); -- Alexander Nasonov

alnsn@yandex.ru wrote:
Peter Dimov wrote:
- you can return a lock from a function: we can make the locks movable regardless of the interface, so it seems possible to ignore this bullet;
- if( lock l = try_lock( m ) ) { }
This is the main difference.
Declarations in conditions are a nice idiom. The typical use is
if( X x = f() ) { // do something with x }
Our example doesn't fit the pattern, however, as the "// do something" part typically does not reference the lock itself.
This is a degenerate case of more general idea of passing lock reference to functions inside condition:
if(lock_ref l = scoped_lock(m)) { // do something with l: action1(l); action2(l); }
Apparently, action1 and action2 can't be called without prior obtaining of a lock. Plus, you get some sort of polymorphism because the functions can accept other locks as well:
action1(scoped_try_lock(m1)); // lock_ref is const reference action2(scoped_timed_lock(m2));
Oh yeah, I heard a talk by Andrei Alexandrescu in which he endorsed that idiom. (Google for "Honey I shrunk the threads".) I like it, but I've never used it. :-P Actually, in the design I was suggesting, there is only one lock type, so you don't have to mess with multiple types, temporaries bound to const references, or a common base class. This idiom becomes easier to use, IMO, with only one lock type. -- Eric Niebler Boost Consulting www.boost-consulting.com

Eric Niebler wrote:
alnsn@yandex.ru wrote:
This is a degenerate case of more general idea of passing lock reference to functions inside condition:
if(lock_ref l = scoped_lock(m)) { // do something with l: action1(l); action2(l); }
Apparently, action1 and action2 can't be called without prior obtaining of a lock. Plus, you get some sort of polymorphism because the functions can accept other locks as well:
action1(scoped_try_lock(m1)); // lock_ref is const reference action2(scoped_timed_lock(m2));
Oh yeah, I heard a talk by Andrei Alexandrescu in which he endorsed that idiom. (Google for "Honey I shrunk the threads".) I like it, but I've never used it. :-P
I know he gave a talk at ACCU this year but I missed the event. And there is nothing on his site yet. Not a problem anymore ;-) Though, I was able to download Kevlin's presentation. -- Alexander Nasonov

If we continue with the upgradable_read_lock idea, I see one danger that needs to be clearly documented. Some "genius" is going to figure out that you can atomically convert a read_lock into a write_lock: rw_mutex m; void invitation_for_deadlock() { read_lock r(m); // ok to read upgradable_read_lock ur(m); r.unlock(); write_lock w(ur); // ok to write } And the next thing we'll know is that the entire NE US is falling into a blackout. ;-) It took me awhile to spot the deadlock in the above code. Does anyone else see the above code as scary? (scary as in looks ok but isn't). Or is the deadlock obvious to everyone else (and thus not scary)? -Howard

Howard Hinnant wrote:
If we continue with the upgradable_read_lock idea, I see one danger that needs to be clearly documented.
Some "genius" is going to figure out that you can atomically convert a read_lock into a write_lock:
rw_mutex m;
void invitation_for_deadlock() { read_lock r(m); // ok to read upgradable_read_lock ur(m); r.unlock(); write_lock w(ur); // ok to write }
And the next thing we'll know is that the entire NE US is falling into a blackout. ;-)
It took me awhile to spot the deadlock in the above code. Does anyone else see the above code as scary? (scary as in looks ok but isn't). Or is the deadlock obvious to everyone else (and thus not scary)?
Not scary, IMO, even if not obvious, because it always deadlocks, as soon as you attempt the "idiom". You can't miss it.

On Jul 9, 2004, at 4:11 PM, Peter Dimov wrote:
rw_mutex m;
void invitation_for_deadlock() { read_lock r(m); // ok to read upgradable_read_lock ur(m); r.unlock(); write_lock w(ur); // ok to write }
And the next thing we'll know is that the entire NE US is falling into a blackout. ;-)
It took me awhile to spot the deadlock in the above code. Does anyone else see the above code as scary? (scary as in looks ok but isn't). Or is the deadlock obvious to everyone else (and thus not scary)?
Not scary, IMO, even if not obvious, because it always deadlocks, as soon as you attempt the "idiom". You can't miss it.
I'm not following "always deadlocks". If only one thread enters, at what point does it deadlock (assuming no other thread playing with m)? Experimentally running through this on my prototype implementation with a single thread isn't deadlocking. But perhaps my prototype is buggy? -Howard

Howard Hinnant wrote:
On Jul 9, 2004, at 4:11 PM, Peter Dimov wrote:
rw_mutex m;
void invitation_for_deadlock() { read_lock r(m); // ok to read upgradable_read_lock ur(m); r.unlock(); write_lock w(ur); // ok to write }
And the next thing we'll know is that the entire NE US is falling into a blackout. ;-)
It took me awhile to spot the deadlock in the above code. Does anyone else see the above code as scary? (scary as in looks ok but isn't). Or is the deadlock obvious to everyone else (and thus not scary)?
Not scary, IMO, even if not obvious, because it always deadlocks, as soon as you attempt the "idiom". You can't miss it.
I'm not following "always deadlocks". If only one thread enters, at what point does it deadlock (assuming no other thread playing with m)? Experimentally running through this on my prototype implementation with a single thread isn't deadlocking. But perhaps my prototype is buggy?
I wouldn't bet on that. Can I change my opinion to "scary"? ;-)

On Jul 9, 2004, at 6:05 PM, Peter Dimov wrote:
Howard Hinnant wrote:
On Jul 9, 2004, at 4:11 PM, Peter Dimov wrote:
rw_mutex m;
void invitation_for_deadlock() { read_lock r(m); // ok to read upgradable_read_lock ur(m); r.unlock(); write_lock w(ur); // ok to write }
And the next thing we'll know is that the entire NE US is falling into a blackout. ;-)
It took me awhile to spot the deadlock in the above code. Does anyone else see the above code as scary? (scary as in looks ok but isn't). Or is the deadlock obvious to everyone else (and thus not scary)?
Not scary, IMO, even if not obvious, because it always deadlocks, as soon as you attempt the "idiom". You can't miss it.
I'm not following "always deadlocks". If only one thread enters, at what point does it deadlock (assuming no other thread playing with m)? Experimentally running through this on my prototype implementation with a single thread isn't deadlocking. But perhaps my prototype is buggy?
I wouldn't bet on that. Can I change my opinion to "scary"? ;-)
<chuckle> Only fools and idiots *never* change their mind! :-) So that's two votes for scary. I'm still undecided if it is "too scary". I.e. will documentation make upgradable_read_lock sufficiently safe? I'm hoping so, because there seems to be a vocal need for that functionality, and I don't see a good alternative at the moment. A good rule of thumb for using rw_mutex seems to be that you should never hold more than one lock on it in the same thread. Now that I write that though, I'm wondering if holding a recursive read_lock might be safe enough.... haven't looked into yet. But holding a read_lock and an upgradable_read_lock in the same thread is definitely ill-advised, even if only for an instant. At great extra expense one could put a runtime check into rw_mutex for one thread owning both a read_mutex and an upgradable_read_mutex. It would require storing a vector<thread_id> for every thread sharing read ownership. That would probably make the rw_mutex so expensive as to be useless though. -Howard

On Fri, 9 Jul 2004 19:33:39 -0400, Howard Hinnant <hinnant@twcny.rr.com> wrote:
So that's two votes for scary. I'm still undecided if it is "too scary". I.e. will documentation make upgradable_read_lock sufficiently safe? I'm hoping so, because there seems to be a vocal need for that functionality, and I don't see a good alternative at the moment.
Three. Too scary.
A good rule of thumb for using rw_mutex seems to be that you should never hold more than one lock on it in the same thread. Now that I write that though, I'm wondering if holding a recursive read_lock might be safe enough.... haven't looked into yet. But holding a read_lock and an upgradable_read_lock in the same thread is definitely ill-advised, even if only for an instant.
I think a base level shareable mutex (rw_mutex) should not have an upgrade from shared to exclusive. An upgradeable one is perhaps another mutex... but I can't see how one is possible without allowing for a failure/retry on contention of the upgrade. To stop the deadlock an optimization would be if I can get the exclusive 'cause I'm the only one sharing then keep going otherwise release my shared lock and block for a write. That is, you get away with just knowing (atomicly) there was only one reader, yourself, then upgrade to a write otherwise you must release your shared and wait for exclusive otherwise you will allow a deadlock. This will break the atomicity of the op which breaks the paradigm. You'd have to redo your state or fail.
At great extra expense one could put a runtime check into rw_mutex for one thread owning both a read_mutex and an upgradable_read_mutex. It would require storing a vector<thread_id> for every thread sharing read ownership. That would probably make the rw_mutex so expensive as to be useless though.
The more I think about the original use case I'm not sure I follow.
From memory the use case was reading stuff and then if that said I needed to write something, write it by upgrading the lock.
However, you are not going to be able to escape the deadlock in this case _ever_. AFAICT was if two things read concurrently and need to change state, the both can't if the states are dependent and thus you always have a deadlock. If they can, then the reading is independent of the writing and you should use different locks, otherwise you need to use an exclusive lock from the outset. I think the upgradeable readlock is a red herring... Regards, Matt Hurd.

On Jul 9, 2004, at 9:47 PM, Matt Hurd wrote:
From memory the use case was reading stuff and then if that said I needed to write something, write it by upgrading the lock.
However, you are not going to be able to escape the deadlock in this case _ever_. AFAICT was if two things read concurrently and need to change state, the both can't if the states are dependent and thus you always have a deadlock.
The upgradable_read_lock, not abused as in the case I showed, will not deadlock. The key (and the restriction) is that only one upgradable_read_lock can be active at a time, though it can share with any number of read_lock's. You are right about the 2-read-to-write promotion case. And that is exactly why upgradable_read_lock's are exclusive among themselves. And it is also why you can't "trick" a read_lock into a promotion (as my abuse-example showed). I believe the upgradable_read_lock is still a viable solution for a very special situation, but it requires suitable documentation to guide the programmer in how not to abuse it. -Howard

On Sat, 10 Jul 2004 15:51:43 -0400, Howard Hinnant <hinnant@twcny.rr.com> wrote:
On Jul 9, 2004, at 9:47 PM, Matt Hurd wrote:
From memory the use case was reading stuff and then if that said I needed to write something, write it by upgrading the lock.
However, you are not going to be able to escape the deadlock in this case _ever_. AFAICT was if two things read concurrently and need to change state, the both can't if the states are dependent and thus you always have a deadlock.
The upgradable_read_lock, not abused as in the case I showed, will not deadlock. The key (and the restriction) is that only one upgradable_read_lock can be active at a time, though it can share with any number of read_lock's. You are right about the 2-read-to-write promotion case. And that is exactly why upgradable_read_lock's are exclusive among themselves. And it is also why you can't "trick" a read_lock into a promotion (as my abuse-example showed).
I believe the upgradable_read_lock is still a viable solution for a very special situation, but it requires suitable documentation to guide the programmer in how not to abuse it.
OK. I buy that, but perhaps as a separate mutex beyound the standared shareable (rw). I suspect some platforms will support rw_mutex like behaviour natively without the upradeable part. The upgradeable shareable lock would work if only one is allowed and it had priority over blocking exclusive locks to prevent state invalidation. It would have to be a special situation where the section of code was not in high contention due to its lack of concurrency (which would be the same for an exclusive lock), with reasonable read concurrency benefits from other sections of code and lowish probability ( at least less than 100% ;-) ) that an upgrade to exclusive (write) access should take place. Does that sound right? I'm not sure I have that correct. Struggling for a use case myself, but it doesn't sound improbable. Perhaps taking a upgradeable read lock or write lock when the thread already has a read, or a write lock when the thread has an upgradeable read lock, should generate an exception, at least in debug mode in addition to the documentation. I think rw_mutex already does this for the read to write from memory. Regards, Matt Hurd.

On Jul 10, 2004, at 10:38 PM, Matt Hurd wrote:
I believe the upgradable_read_lock is still a viable solution for a very special situation, but it requires suitable documentation to guide the programmer in how not to abuse it.
OK. I buy that, but perhaps as a separate mutex beyound the standared shareable (rw). I suspect some platforms will support rw_mutex like behaviour natively without the upradeable part.
<nod> Perhaps so. -Howard

I'm wondering about the following scenario (with whatever syntax): try_mutex m1; rw_mutex rwm; void foo() { rw_mutex::upgradable_read_lock ur(rwm); ... if (...) { rw_mutex::write_lock w(m, false); // Need to write lock rwm and lock m1 here (for whatever reason) } } This implies a use of lock_both, which requires two try_locks. I can build a generic try_lock to handle transferring from ur to w like so: template <class TryLock1, class TryLock2> class transfer_lock { public: transfer_lock(TryLock1& m1, TryLock2& m2, bool lock_it = true); ~transfer_lock(); void lock(); bool try_lock(); void unlock(); bool locked() const; operator int bool_type::* () const; private: transfer_lock(const transfer_lock&); transfer_lock& operator=(const transfer_lock&); }; This is just an extension of the helper lock class I presented earlier with try_lock functionality added. transfer_lock::try_lock() will need to try to transfer ownership from m2 (the upgradable_read_lock) to m1 (the write_lock) in a non-blocking manner. This will require some additional interface on upgradable_read_lock, possibly: template <class RW_Mutex> class upgradable_read_lock { public: ... bool try_transfer_to(write_lock<mutex_type>& w); ... }; So the end user code might look like: try_mutex m1; rw_mutex rwm; void foo() { typedef rw_mutex::upgradable_read_lock UR; typedef rw_mutex::write_lock WL; typedef transfer_lock<WL,UR> TL; typedef try_mutex::lock OtherLock; UR ur(rwm); ... if (...) { WL w(m, false); TL tl(w, ur, false); OtherLock l1(m1, false); lock_both<TL, OtherLock> lock(tl, l1); // ok, ur unlocked, w locked, and l1 locked, all atomically } } Questions: 1. Does anyone see a disaster (deadlock or otherwise) in trying to do all this atomically? I so far haven't seen any problems, but that doesn't mean much. 2. If there are no problems, does anyone see this use scenario as likely to be needed, or worth the trouble? Notes: This begs the question of adding: template <class RW_Mutex> class read_lock { public: ... bool try_transfer_to(write_lock<mutex_type>& w); ... }; However I strongly recommend against it. Substituting read_lock in for upgradable_read_lock in the previous code: try_mutex m1; rw_mutex rwm; void foo() { typedef rw_mutex::read_lock RL; typedef rw_mutex::write_lock WL; typedef transfer_lock<WL,RL> TL; typedef try_mutex::lock OtherLock; RL r(rwm); ... if (...) { WL w(m, false); TL tl(w, r, false); OtherLock l1(m1, false); lock_both<TL, OtherLock> lock(tl, l1); // error, deadlock possible! } } If two threads try to construct the lock_both simultaneously (which isn't possible with the upgradable_read_lock version) then neither will be able to obtain the write_lock w because each will be holding the read_lock r and refusing to let it go. Therefore I believe the try_transfer_to(write_lock) functionality for read_lock is inherently dangerous and shouldn't be provided. Comments? -Howard

Peter Dimov wrote:
Eric Niebler wrote:
1) a reduction in interface complexity. - one lock class, instead of a scoped_lock, try_lock, timed_lock, etc. - no bools or enums in constructors; instead, there are clearly named factory functions for customizing lock construction.
2) You can initialize a lock in the condition of an "if" statement.
3) You can return a lock from a function. (Benefit is debatable.)
After giving it some thought:
- one lock class: tie;
This seems both popular and reasonable. I presume it would be templated on the mutex type?
- no bools or enums in constructors: there is a bool argument, but it's (in my opinion) acceptable since there is a single constructor, and the argument participates directly in its postcondition:
Lock( Mutex & m, bool l = true ); Post: locked() == l;
No argument here, either. I would mildly prefer a couple more constructors, as you know, but I do see understand your arguments for a single constructor and see the advantages of that design.
- you can return a lock from a function: we can make the locks movable regardless of the interface, so it seems possible to ignore this bullet;
I agree.
- if( lock l = try_lock( m ) ) { }
This is the main difference.
Declarations in conditions are a nice idiom. The typical use is
if( X x = f() ) { // do something with x }
Our example doesn't fit the pattern, however, as the "// do something" part typically does not reference the lock itself.
I don't have much experience with try locks, though. Pretty much the only example that comes to mind is Howard's lock_both:
for(;;) { l1.lock(); if( l2.try_lock() ) return; l1.unlock();
l2.lock(); if( l1.try_lock() ) return; l2.unlock(); }
(I hope I got that right) and I don't see how it can be improved by the if() idiom.
This idiom becomes possible when locks become transferable / returnable anyway, so I don't think we need to worry about it now, either. Mike

Michael Glassford wrote:
Peter Dimov wrote:
After giving it some thought:
- one lock class: tie;
This seems both popular and reasonable. I presume it would be templated on the mutex type?
Seems entirely reasonable. Perhaps it's time to move to formal wording. template<class M> class scoped_lock { public: explicit scoped_lock( M & m, bool l = true ); // effects: if( l ) m.lock(); // post: locked() == l && mutex() == &m; ~scoped_lock(); // effects: if( locked() ) mutex()->unlock(); void lock(); // throws: lock_aready_locked if locked(); // effects: mutex()->lock(); // post: locked(); bool try_lock(); // throws: lock_aready_locked if locked(); // returns: mutex()->try_lock(); // post: locked() == (return value); bool timed_lock( xtime xt ); // throws: lock_aready_locked if locked(); // returns: mutex()->timed_lock( xt ); // post: locked() == (return value); void unlock(); // throws: lock_not_locked when !locked(); // effects: mutex()->unlock(); // post: !locked(); bool locked() const; Mutex * mutex() const; // returns: the associated mutex; operator unspecified-bool-type() const; // returns: locked(). }; I've added the mutex() accessor to support Andrei's lock idiom: void f() { scoped_lock lock( my_mutex ); f( lock ); } void f( scoped_lock & lock ) { // check for lock validity assert( lock.locked() && lock.mutex() == &my_mutex ); // proceed with operation } Now that we got rid of the excess locks, how about doing the same with the mutexes? class mutex // DefaultConstructible { private: void lock(); // pre: *this is not locked by the current thread // effects: blocks until *this is unlocked // post: *this is locked by the current thread bool try_lock(); // returns: true iff *this was unlocked // post: if *this was unlocked, *this is now locked by the current thread bool timed_lock( xtime xt ); // returns: true if *this is now locked by the current thread // effects: if *this is unlocked, returns immediately, otherwise blocks // until either *this is unlocked or xt has been reached // post: if *this was or became unlocked, *this is now locked by the // current thread void unlock(); // pre: *this is locked by the current thread // post: *this is unlocked }; The main potential issue here is mutex::timed_lock; try_lock doesn't seem controversial. It is true that Windows 95 does not have TryEnterCriticalSection, but an implementation can use Alexander Terekhov's alternative: http://lists.boost.org/MailArchives/boost/msg64648.php The mutex::timed_lock situation is a bit more complicated. POSIX labels pthread_mutex_timedlock as part of the Timeouts option, and I don't know whether this is because many implementations deliberatley do not provide timed locks, or because this is a relatively new addition to pthreads. Does someone know? My not-so-informed opinion at the moment is that we should provide timed_lock. I see that the current Boost.Threads implementation uses a mutex and a condition variable to implement a timed_lock-capable mutex, so we have a proof of concept that it can always be done, however there may be efficiency concerns. My line of thought is that since a mutex (usually) has a fast path user space portion and a slow path kernel space portion, with the timed_lock baggage not affecting the fast path, it seems reasonable to always require timed_lock (remember we're in "next standard mode" now, thinking severals years ahead). Thoughts? Some more random remarks regarding the state of Boost.Threads. Several files still seem to have incorrect line endings. This usually happens when checking in Windows line endings with a Unix CVS client. Please don't do that. I was somewhat surprised by the mutex/cv implementation. I expected a thin wrapper over pthreads, but this is not the case. At first sight it seems that the complexity is caused by the fact that boost::condition supports recursive mutexes that are locked more than once, whereas POSIX does not. I do not recall any discussions about this issue, and I'm not sure why this decision was made. It seems wrong (and the implementation seems buggy), but as usual, I may be missing something.

Peter Dimov wrote:
Michael Glassford wrote:
Peter Dimov wrote:
After giving it some thought:
- one lock class: tie;
This seems both popular and reasonable. I presume it would be templated on the mutex type?
Seems entirely reasonable. Perhaps it's time to move to formal wording.
template<class M> class scoped_lock { public:
explicit scoped_lock( M & m, bool l = true ); // effects: if( l ) m.lock(); // post: locked() == l && mutex() == &m;
~scoped_lock(); // effects: if( locked() ) mutex()->unlock();
void lock(); // throws: lock_aready_locked if locked(); // effects: mutex()->lock(); // post: locked();
bool try_lock(); // throws: lock_aready_locked if locked(); // returns: mutex()->try_lock(); // post: locked() == (return value);
bool timed_lock( xtime xt ); // throws: lock_aready_locked if locked(); // returns: mutex()->timed_lock( xt ); // post: locked() == (return value);
void unlock(); // throws: lock_not_locked when !locked(); // effects: mutex()->unlock(); // post: !locked();
bool locked() const;
Mutex * mutex() const; // returns: the associated mutex;
operator unspecified-bool-type() const; // returns: locked(). };
Yes, this is much what I had in mind. The mutex type(s) would continue to supply a typedef, I presume (but only one): class some_mutex_type { public: typedef lock<some_mutex_type> scoped_lock; //... }; Also, although there may only be one lock class, there are still three lock concepts specifying which lock operations are supported by each of the three mutex concepts, right? (Unless we actually do combine all of the mutex types into one as well.)
I've added the mutex() accessor to support Andrei's lock idiom:
OK.
void f() { scoped_lock lock( my_mutex ); f( lock ); }
void f( scoped_lock & lock ) { // check for lock validity assert( lock.locked() && lock.mutex() == &my_mutex );
// proceed with operation }
Now that we got rid of the excess locks, how about doing the same with the mutexes?
I had considered this, but there does seem to be some benefit in having separate mutex types, which is what I assume led to there being three mutex types and three lock types in the original design. You've noted these reasons below, but I'll reiterate: * On pthreads, the timed mutex requires an additional data member, (condition variable) to handle cases when pthreads_timedlock isn't supported. * On WinNT, the timed mutex operations require a win32 mutex object, while the mutex and try mutex can use a win32 critical section. * On Win9x, the timed mutex and try mutex operations require a win32 mutex object, while the mutex can use a win32 critical section. In other words, on the most widely-used platforms, collapsing the mutex types into one imposes some penalty (larger mutex object or more limited implementation options) on users. Also (I ask, not as a hypothetical question, but as a request for information): is there another platform where combining the mutex types incurs a similar or worse penalty?
class mutex // DefaultConstructible { private:
void lock(); // pre: *this is not locked by the current thread // effects: blocks until *this is unlocked // post: *this is locked by the current thread
bool try_lock(); // returns: true iff *this was unlocked // post: if *this was unlocked, *this is now locked by the current thread
bool timed_lock( xtime xt ); // returns: true if *this is now locked by the current thread // effects: if *this is unlocked, returns immediately, otherwise blocks // until either *this is unlocked or xt has been reached // post: if *this was or became unlocked, *this is now locked by the // current thread
void unlock();
// pre: *this is locked by the current thread // post: *this is unlocked };
The main potential issue here is mutex::timed_lock; try_lock doesn't seem controversial. It is true that Windows 95 does not have TryEnterCriticalSection, but an implementation can use Alexander Terekhov's alternative:
The Boost.Threads try_mutex in CVS currently checks if TryEnterCriticalSection is available and uses it if it is. To me this seems a reasonable implementation for now, and if it proves not to be good enough, a TryEnterCriticalSection facsimile can be implemented for Win9x platforms, as you say.
http://lists.boost.org/MailArchives/boost/msg64648.php
The mutex::timed_lock situation is a bit more complicated. POSIX labels pthread_mutex_timedlock as part of the Timeouts option, and I don't know whether this is because many implementations deliberatley do not provide timed locks, or because this is a relatively new addition to pthreads. Does someone know?
My not-so-informed opinion at the moment is that we should provide timed_lock. I see that the current Boost.Threads implementation uses a mutex and a condition variable to implement a timed_lock-capable mutex, so we have a proof of concept that it can always be done, however there may be efficiency concerns.
OK.
My line of thought is that since a mutex (usually) has a fast path user space portion and a slow path kernel space portion, with the timed_lock baggage not affecting the fast path, it seems reasonable to always require timed_lock (remember we're in "next standard mode" now, thinking severals years ahead).
I think I'm being dense, but I'm not sure I understand what you mean by "require timed_lock" (require platform support for it?) or what this part of the paragraph is getting at.
Thoughts?
Some more random remarks regarding the state of Boost.Threads. Several files still seem to have incorrect line endings. This usually happens when checking in Windows line endings with a Unix CVS client. Please don't do that.
I don't. I typically use the VC++ 7.1 editor and TortoiseCVS. I seem to remember a problem like this being discussed in the Spirit mailing list with an earlier TortoiseCVS, but I'm using the latest version that was supposed to have fixed that problem.
I was somewhat surprised by the mutex/cv implementation. I expected a thin wrapper over pthreads, but this is not the case. At first sight it seems that the complexity is caused by the fact that boost::condition supports recursive mutexes that are locked more than once, whereas POSIX does not. I do not recall any discussions about this issue, and I'm not sure why this decision was made.
I can't comment on this since I haven't yet really looked at the condition variable implementation and had nothing to do with the original decisions.
It seems wrong (and the implementation seems buggy), but as usual, I may be missing something.
Alexander Terekhov has mentioned one problem with the Win32 condition variable implementation that I recall (I have the link somewhere but don't see it at the moment); do you have others in mind? Mike

Michael Glassford <glassfordm@hotmail.com> writes:
I had considered this, but there does seem to be some benefit in having separate mutex types, which is what I assume led to there being three mutex types and three lock types in the original design. You've noted these reasons below, but I'll reiterate:
* On pthreads, the timed mutex requires an additional data member, (condition variable) to handle cases when pthreads_timedlock isn't supported.
* On WinNT, the timed mutex operations require a win32 mutex object, while the mutex and try mutex can use a win32 critical section.
* On Win9x, the timed mutex and try mutex operations require a win32 mutex object, while the mutex can use a win32 critical section.
In other words, on the most widely-used platforms, collapsing the mutex types into one imposes some penalty (larger mutex object or more limited implementation options) on users. Also (I ask, not as a hypothetical question, but as a request for information): is there another platform where combining the mutex types incurs a similar or worse penalty?
If the difference is in the code that must be generated within the locking function, I'd guess that the differently-named functions can handle it. If the difference is in the data stored in the mutex and/or the lock, it could be an issue. You might be able to use boost::variant to minimize storage. It would be worth doing some experimentation to see whether optimizers can make the other overheads disappear. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

Finally getting time to answering this: David Abrahams wrote:
Michael Glassford <glassfordm@hotmail.com> writes:
I had considered this, but there does seem to be some benefit in having separate mutex types, which is what I assume led to there being three mutex types and three lock types in the original design. You've noted these reasons below, but I'll reiterate:
* On pthreads, the timed mutex requires an additional data member, (condition variable) to handle cases when pthreads_timedlock isn't supported.
* On WinNT, the timed mutex operations require a win32 mutex object, while the mutex and try mutex can use a win32 critical section.
* On Win9x, the timed mutex and try mutex operations require a win32 mutex object, while the mutex can use a win32 critical section.
In other words, on the most widely-used platforms, collapsing the mutex types into one imposes some penalty (larger mutex object or more limited implementation options) on users. Also (I ask, not as a hypothetical question, but as a request for information): is there another platform where combining the mutex types incurs a similar or worse penalty?
If the difference is in the code that must be generated within the locking function, I'd guess that the differently-named functions can handle it. If the difference is in the data stored in the mutex and/or the lock, it could be an issue. You might be able to use boost::variant to minimize storage. It would be worth doing some experimentation to see whether optimizers can make the other overheads disappear.
The difference in the pthreads case is a difference in the data that must be stored in the Boost.Threads mutex object: each one needs to store both a mutex and a condition variable, though I suppose--at the expense of requiring the storage to be dynamically allocated--space for the condition variable could be allocated only on platforms that don't support pthreads_timedlock. In the Win32 cases, if you're going to use timed mutex functions (or, on Win9x, try mutex functions), the Boost.Threads mutex object needs to allocate a Win32 mutex object; if not, it can allocate a Win32 critical section. Currently Boost.Threads mutexes objects can tell whether you intend to use try or timed functions by whether you declare a try_mutex or timed_mutex, and allocate the appropriate Win32 synchronization object accordingly. But if all mutex types are combined into one there won't be any way for it to tell, and it will have to allocate a Win32 mutex object always. Another option in the Win32 case, which Peter already mentioned, is to write an alternate implementation of critical sections (such as the one Alexander Terekhov outlined an implementation for) that supports both try and timed functions. If done well, this would be desirable even if the mutex types are not combined. Mike

Michael Glassford wrote: [...]
The difference in the pthreads case is a difference in the data that must be stored in the Boost.Threads mutex object: each one needs to store both a mutex and a condition variable, though I suppose--at the expense of requiring the storage to be dynamically allocated--space for the condition variable could be allocated only on platforms that don't support pthreads_timedlock.
No, no. The goal of mutex::timed_lock is not to cripple the mutex by turning it into a monitor. This is unacceptable. The goal is to provide an efficient timed_lock. If there is no efficient timed lock available on the platform, there should be no mutex::timed_lock. The argument we had with Howard is not about the above, on which we - hopefully - agree. The argument was about whether, from standard point of view, an implementation that doesn't provide mutex::timed_lock should be conforming. It is mostly a specification/theory/philosophy issue. In practice, the rule is simple. Have _POSIX_TIMEOUTS -> will travel, er, will have pthread_mutex_timedlock and mutex::timed_lock. The idea is that our C++ mutex should always be competitive with the "best" native mutex available on the platform. (Incidentally, on Windows, the best native mutex is neither CRITICAL_SECTION nor HMUTEX, it's Alexander's event-based thing. Or so he says. But he's usually right about these things.)

Peter Dimov wrote:
Michael Glassford wrote:
[...]
The difference in the pthreads case is a difference in the data that must be stored in the Boost.Threads mutex object: each one needs to store both a mutex and a condition variable, though I suppose--at the expense of requiring the storage to be dynamically allocated--space for the condition variable could be allocated only on platforms that don't support pthreads_timedlock.
No, no.
The goal of mutex::timed_lock is not to cripple the mutex by turning it into a monitor. This is unacceptable. The goal is to provide an efficient timed_lock. If there is no efficient timed lock available on the platform, there should be no mutex::timed_lock.
Sorry, the context doesn't make it clear but my paragraph above referred to the timed_mutex class which, according to what I remember of your discussions with Howard, you (?) suggested would implement a timed lock regardless of whether it was supported by the platform and regardless of whether it could be implemented efficiently. I wasn't referring to mutex::timed_lock at all.
The argument we had with Howard is not about the above, on which we - hopefully - agree. The argument was about whether, from standard point of view, an implementation that doesn't provide mutex::timed_lock should be conforming. It is mostly a specification/theory/philosophy issue. In practice, the rule is simple. Have _POSIX_TIMEOUTS -> will travel, er, will have pthread_mutex_timedlock and mutex::timed_lock.
I have to say that I'm not too keen on the idea of an optional timed_lock() member function that only exists if it can be implemented efficiently, but that's another discussion.
The idea is that our C++ mutex should always be competitive with the "best" native mutex available on the platform.
It's hard to disagree with this.
(Incidentally, on Windows, the best native mutex is neither CRITICAL_SECTION nor HMUTEX, it's Alexander's event-based thing. Or so he says. But he's usually right about these things.)
Yes, and I hope to look into it when I get time. Mike

Michael Glassford wrote:
Sorry, the context doesn't make it clear but my paragraph above referred to the timed_mutex class...
I see; yes, in this case, you are right that timed_mutex needs to continue holding a mutex and a condition variable in the general case. There is no need to allocate a condition variable dynamically, however, as the existence of pthread_mutex_timedlock is a compile-time thing.
... which, according to what I remember of your discussions with Howard, you (?) suggested would implement a timed lock regardless of whether it was supported by the platform and regardless of whether it could be implemented efficiently.
No, I did not suggest that; this is simply the current status quo, which Howard likes to see preserved. I suggested removing timed_mutex from the specification, leaving it in the implementation only to support backwards compatibility. My argument was that forcing the user to explicitly spell the mutex+condition combination hidden by the timed_mutex may prompt said user to reevaluate and improve the design, but this may be overly optimistic.

Michael Glassford wrote:
The idea is that our C++ mutex should always be competitive with the "best" native mutex available on the platform.
It's hard to disagree with this.
(Incidentally, on Windows, the best native mutex is neither CRITICAL_SECTION nor HMUTEX, it's Alexander's event-based thing. Or so he says. But he's usually right about these things.)
Yes, and I hope to look into it when I get time.
I'm not a Windows programmer, so feel free to disregard anything that follows, but weren't events considered and rejected already? From http://www.boost.org/libs/thread/doc/rationale.html#events: "Experienced programmers using the Windows platform today report that event variables are a continuing source of errors, even after previous bad experiences caused them to be very careful in their use of event variables. Overt problems can be avoided, for example, by teaming the event variable with a mutex, but that may just convert a race condition into another problem, such as excessive resource use. One of the most distressing aspects of the experience reports is the claim that many defects are latent. That is, the programs appear to work correctly, but contain hidden timing dependencies which will cause them to fail when environmental factors or usage patterns change, altering relative thread timings. The decision to exclude event variables from Boost.Threads has been surprising to some Windows programmers. They have written programs which work using event variables, and wonder what the problem is. It seems similar to the "goto considered harmful" controversy of 30 years ago. It isn't that events, like gotos, can't be made to work, but rather that virtually all programs using alternatives will be easier to write, debug, read, maintain, and be less likely to contain latent defects." This is not to say that "Alexander's event-based thing" can't be done correctly, but I feel like this is covered ground. -- Christopher Currie <codemonkey@gmail.com>

"Peter Dimov" <pdimov@mmltd.net> writes:
Alexander's event-based thing. Or so he says. But he's usually right about these things
Wow, that's interesting. I haven't learned much about threading, but one thing that was drilled into me long ago is that Windows events have giant safety holes in 'em. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote: [...]
Windows events have giant safety holes in 'em.
That's correct. The motto is "don't have sex with events without condoms." regards, alexander. -- post_pre<post_pre<post_pre<MostDerived, Base>, Derived>, MostDerived> o;

Michael Glassford wrote:
Peter Dimov wrote:
It seems wrong (and the implementation seems buggy), but as usual, I may be missing something.
Alexander Terekhov has mentioned one problem with the Win32 condition variable implementation that I recall (I have the link somewhere but don't see it at the moment); do you have others in mind?
For a condition being used with a recursive_mutex, condition::wait does: typename lock_ops::lock_state state; lock_ops::unlock(mutex, state); m_impl.do_wait(...); lock_ops::lock(mutex, state); The (Windows, for example) recursive mutex stores its lock count in state: void recursive_mutex::do_unlock(cv_state& state) { state = m_count; m_count = 0; if (m_critical_section) release_critical_section(m_mutex); else release_mutex(m_mutex); } and restores it in do_lock( cv_state& ). However, if there is a thread switch just after m_count = 0, and then the recursive_mutex is locked, its m_count would now be 1. After the do_wait, do_lock( cv_state& ) will restore the m_count to the original value, and the count update caused by the "extra" lock would be lost. Oder?

Peter Dimov wrote:
Michael Glassford wrote:
Peter Dimov wrote:
It seems wrong (and the implementation seems buggy), but as usual, I may be missing something.
Alexander Terekhov has mentioned one problem with the Win32 condition variable implementation that I recall (I have the link somewhere but don't see it at the moment); do you have others in mind?
For a condition being used with a recursive_mutex, condition::wait does:
typename lock_ops::lock_state state; lock_ops::unlock(mutex, state); m_impl.do_wait(...); lock_ops::lock(mutex, state);
The (Windows, for example) recursive mutex stores its lock count in state:
void recursive_mutex::do_unlock(cv_state& state) { state = m_count; m_count = 0;
if (m_critical_section) release_critical_section(m_mutex); else release_mutex(m_mutex); }
and restores it in do_lock( cv_state& ).
However, if there is a thread switch just after m_count = 0, and then the recursive_mutex is locked, its m_count would now be 1. After the do_wait, do_lock( cv_state& ) will restore the m_count to the original value, and the count update caused by the "extra" lock would be lost. Oder?
When I first read this it made sense to me, but now I'm not seeing it. It's not possible for another thread (thread B) to lock the recursive mutex just after thread A executes m_count = 0 because thread A still holds the m_mutex, right? Mike

Michael Glassford wrote:
Peter Dimov wrote:
[Boost.Threads cv on recursive_mutex]
When I first read this it made sense to me, but now I'm not seeing it. It's not possible for another thread (thread B) to lock the recursive mutex just after thread A executes m_count = 0 because thread A still holds the m_mutex, right?
I think I was wrong. The situation I described is a non-problem.

On Jul 11, 2004, at 8:46 AM, Peter Dimov wrote:
Seems entirely reasonable. Perhaps it's time to move to formal wording.
template<class M> class scoped_lock { public:
explicit scoped_lock( M & m, bool l = true ); // effects: if( l ) m.lock(); // post: locked() == l && mutex() == &m;
~scoped_lock(); // effects: if( locked() ) mutex()->unlock();
void lock(); // throws: lock_aready_locked if locked(); // effects: mutex()->lock(); // post: locked();
bool try_lock(); // throws: lock_aready_locked if locked(); // returns: mutex()->try_lock(); // post: locked() == (return value);
bool timed_lock( xtime xt ); // throws: lock_aready_locked if locked(); // returns: mutex()->timed_lock( xt ); // post: locked() == (return value);
void unlock(); // throws: lock_not_locked when !locked(); // effects: mutex()->unlock(); // post: !locked();
bool locked() const;
Mutex * mutex() const; // returns: the associated mutex;
Why return a pointer instead of a reference? What about this instead? Mutex& mutex(); const Mutex& mutex() const; That would put to rest any questions about mutex() transferring mutex ownership, e.g.: delete lock.mutex();
operator unspecified-bool-type() const; // returns: locked(). };
I've added the mutex() accessor to support Andrei's lock idiom:
void f() { scoped_lock lock( my_mutex ); f( lock ); }
void f( scoped_lock & lock ) { // check for lock validity assert( lock.locked() && lock.mutex() == &my_mutex );
// proceed with operation }
Now that we got rid of the excess locks, how about doing the same with the mutexes?
That worries me a lot. We need several different flavors of mutex because the more functionality you put into a mutex, the more expensive it is both in terms of size and speed. Why pay for a recursive mutex if you don't need one? Why pay for a timed mutex? But when you need one of these more expensive types, then the price is worth it. We can get away with a scoped/try/timed lock precisely because lumping that functionality into these templated classes does not add overhead (the overhead is in the mutex).
I was somewhat surprised by the mutex/cv implementation. I expected a thin wrapper over pthreads, but this is not the case. At first sight it seems that the complexity is caused by the fact that boost::condition supports recursive mutexes that are locked more than once, whereas POSIX does not. I do not recall any discussions about this issue, and I'm not sure why this decision was made. It seems wrong (and the implementation seems buggy), but as usual, I may be missing something.
I can't speak for the boost implementation (haven't carefully studied it), but the Metrowerks implementation also supports conditions operating on a recursive mutex. Our implementation (and I strongly suspect boost's as well) also supports conditions operating on non-recursive mutexes. The condition::wait<NonRecursiveMutex> /is/ a thin wrapper around pthread_cond_wait (when implemented on pthreads). Overhead is only incurred for condtion::wait<RecursiveMutex>. On the other hand, our Windows implementation of condition::wait follows the algorithm laid out by Alexander Terekhov's "Algorithm 8a" posted in comp.programming.threads on April 27, 2001 (and noted as such in that file Alexander). In this case the wait() function does not differ depending upon the recursiveness of the mutex. This underscores the importance of having different types of mutexes. Adding functionality to a mutex (beyond a non-recursive, non-try, non-timed mutex) may well add significant overhead on some platforms, whether or not you actually use that added functionality. -Howard

Howard Hinnant wrote:
On Jul 11, 2004, at 8:46 AM, Peter Dimov wrote:
[...]
Mutex * mutex() const; // returns: the associated mutex;
Why return a pointer instead of a reference? What about this instead?
Mutex& mutex(); const Mutex& mutex() const;
Because of the use case shown below:
void f( scoped_lock & lock ) { // check for lock validity assert( lock.locked() && lock.mutex() == &my_mutex );
// proceed with operation }
A pointer makes it clear that it is the identity of the associated mutex that is being queried. A (const-"correct") reference return implies that the purpose of the accessor is to return, well, a reference to the mutex object, presumably so that the client can do something with it.
That would put to rest any questions about mutex() transferring mutex ownership, e.g.:
delete lock.mutex();
I considered bool is_associated_with( Mutex const & m ) const; but that's a bit too much of a handholding for my taste, and doesn't allow us to use mutex() in the Effects clauses. The kind of a person that would delete lock.mutex() would never get a multithreaded program correct anyway.
Now that we got rid of the excess locks, how about doing the same with the mutexes?
That worries me a lot. We need several different flavors of mutex because the more functionality you put into a mutex, the more expensive it is both in terms of size and speed. Why pay for a recursive mutex if you don't need one? Why pay for a timed mutex? But when you need one of these more expensive types, then the price is worth it.
First, please note that I never said anything about mutex being recursive (and the specification says that relocking is undefined behavior). A recursive mutex is a separate entity (and its specification is slightly different). That aside. It is precisely the assertion that (1) a try_mutex is more expensive than a mutex, and (2) that a timed_mutex is more expensive than a try_mutex, that I am challenging. I see no evidence for (1). (2) is more interesting. You will note that POSIX indeed makes you explicitly state whether you want a recursive mutex or not - because recursive mutexes are more expensive. However no such requirement exists for "timed mutexes". Either all mutexes are timed, or they are not. This, for me, indicates that a timed mutex is not more expensive than a try mutex, just that pthread_mutex_timedlock is a late addition to the standard. Do you have data on whether your platforms support pthread_mutex_timedlock?
I can't speak for the boost implementation (haven't carefully studied it), but the Metrowerks implementation also supports conditions operating on a recursive mutex.
Interesting. I presume that this was a deliberate design decision. There is a school of thought that says that a recursive mutex can only be used to sweep bugs under the carpet and is never needed in a correctly designed multithreaded program, which probably applies doubly to conditions operating on a recursive mutex locked more than once. But I'm not qualified to judge.

Peter Dimov wrote: [... Mutex * mutex() const ...] And the Mutex thing (std0X::whatever) shall also provide something like pthread_mutex_t * c_mutex(); pthread_mutex_t const * c_mutex() const; That pthread_mutex_t shall be defined in <cthread> (and it shall of course also be available through *deprecated* <pthread.h>) and shall perform dynamic dispatching for "C-style" code (depending on "C++ mutex type"). Oder?
I can't speak for the boost implementation (haven't carefully studied it), but the Metrowerks implementation also supports conditions operating on a recursive mutex.
Interesting. I presume that this was a deliberate design decision.
It has a precondition "lock.mutex()->locked() && lock.mutex()-> lock_count() == 1" (or something like that... brrrr, so many "lock" things... "guard" would sound much better ;-) ) regards, alexander.

Alexander Terekhov wrote:
Peter Dimov wrote:
[... Mutex * mutex() const ...]
And the Mutex thing (std0X::whatever) shall also provide something like
pthread_mutex_t * c_mutex(); pthread_mutex_t const * c_mutex() const;
That pthread_mutex_t shall be defined in <cthread> (and it shall of course also be available through *deprecated* <pthread.h>) and shall perform dynamic dispatching for "C-style" code (depending on "C++ mutex type"). Oder?
Forget that <cthread> for a while, how about sharing your thoughts on the proposed elimination of the try/timed axis. ;-)
I can't speak for the boost implementation (haven't carefully studied it), but the Metrowerks implementation also supports conditions operating on a recursive mutex.
Interesting. I presume that this was a deliberate design decision.
It has a precondition "lock.mutex()->locked() && lock.mutex()-> lock_count() == 1" (or something like that... brrrr, so many "lock" things... "guard" would sound much better ;-) )
Does it? I think that the Boost implementation tries to operate "correctly" for lock_count() > 1, but I may have misread the code.

Peter Dimov wrote: [...]
Forget that <cthread> for a while, how about sharing your thoughts on the proposed elimination of the try/timed axis. ;-)
Oh yeah. ;-) In the past (before "swap_based_mutex"... having only something along the lines of old/current pthread-win32's mutex thing) and given PTHREAD_TIMEOUT_ALLOWED/PTHREAD_TIMEOUT_DISALLOWED (stuff that was proposed--among other interesting "military" things--in 1003.1d/D11 and was also ditched later), I had some reservations... http://groups.google.com/groups?selm=3F096C73.6A3DCA60%40web.de http://groups.google.com/groups?selm=3f0c0a3d%40usenet01.boi.hp.com http://groups.google.com/groups?selm=3F0C0F9E.FACB8EA3%40web.de (If you read this entire message read also 3F0E944B.68DB9DCA%40web.de) ---- The point is that a mere presence of timedlock() may slow down pthread_mutex_unlock(), for example. [reference to pthreads-win32] ---- but not now.
I can't speak for the boost implementation (haven't carefully studied it), but the Metrowerks implementation also supports conditions operating on a recursive mutex.
Interesting. I presume that this was a deliberate design decision.
It has a precondition "lock.mutex()->locked() && lock.mutex()-> lock_count() == 1" (or something like that... brrrr, so many "lock" things... "guard" would sound much better ;-) )
Does it? I think that the Boost implementation tries to operate "correctly" for lock_count() > 1, but I may have misread the code.
I meant windows thing that is used in Metrowerks [if I got Howard right] and pthread-win32. IIRC, I've "voiced objection" at the time the boost::condition and recursive locks were discussed here. Uhmm, can't find it... but here's something: ;-) http://groups.google.com/groups?selm=3D484233.AD1E26E8%40web.de regards, alexander.

Alexander Terekhov wrote:
http://groups.google.com/groups?selm=3f0c0a3d%40usenet01.boi.hp.com
"Had I been following 1003.1d when they added this silly attribute I would have objected. Since it was removed, clearly there were plenty of others with sense available. ;-) The critical aspect of mutex performance is in the NONBLOCKING case, where timeout is irrelevant. The cost of setting up for a timeout on the blocking path is tiny compared to the scheduling operations, and would only occur when calling pthread_mutex_timedlock(). (Which presumably one would never do on a mutex that would have been set PTHREAD_TIMEOUT_DISALLOWED.)" -- Butenhof
Does it? I think that the Boost implementation tries to operate "correctly" for lock_count() > 1, but I may have misread the code.
I meant windows thing that is used in Metrowerks [if I got Howard right] and pthread-win32. IIRC, I've "voiced objection" at the time the boost::condition and recursive locks were discussed here. Uhmm, can't find it...
http://lists.boost.org/MailArchives/boost-users/msg03835.php http://lists.boost.org/MailArchives/boost-users/msg03838.php "BTW, Mr. Terekhov does provide a good resource in his posting on this subject. I disagree with some points in the thread he posted, but it is quite true that you absolutely must gaurantee that invariants hold at the point cond.wait() is called, and that doing so is complicated by the use of recursive locks. Either avoid recursive locks entirely, avoid condition variables with recursive mutexes, or be very sure of the design and the state of your invariants." -- Kempf http://groups.google.com/groups?threadm=34293AAA.69FB%40opentext.com (the aforementioned resource) "Unfortunately, recursive mutexes are a bad idea. They're expedient, and it is, sometimes, useful to have them available so you don't need to make code thread-safe. But there's ALWAYS a better, more maintainable, and far more efficient alternative that uses normal mutexes. For a long time, I regretted having introduced recursive mutexes into DCE (which is what caused them to become part of XSH5). I've since reached a state of relative inner peace, however. They CAN BE convenient. If used properly (that is, with careful analysis and design), recursive mutexes can be an effective way to use code that's never performance-critical in threaded programs. (But if you've got code that's ever on a hot path, avoid recursive mutexes like the plague that they are!)" -- Butenhof http://groups.google.com/groups?selm=3434E6BA.ABD%40zko.dec.com and the rest of the thread. Executive summary: mutexes protect temporarily broken invariants. condition::wait() can safely unlock once because the code that called wait() knows this to be safe (it locked the mutex). However multiple unlockings are unsafe, because if you knew they'd be safe, you'd have had enough knowledge to avoid the recursive_mutex, too.
but here's something: ;-)
http://groups.google.com/groups?selm=3D484233.AD1E26E8%40web.de
"AFAICS, boost::condition's use of >>boost::recursive_mutex<< is totally broken [and its utterly brain-dead/java-like N->0 ``spin out feature'' aside for a moment], to begin with." -- Terekhov

Alexander Terekhov wrote:
Peter Dimov wrote: [...]
Forget that <cthread> for a while, how about sharing your thoughts on the proposed elimination of the try/timed axis. ;-)
Oh yeah. ;-) [...]
This is where it all started: http://lists.boost.org/MailArchives/boost/msg10142.php I have to admit, Bill Kempf had gotten many things in the original design right, including a single boost::mutex and a reference counted body + handle boost::thread class. I even participated in that discussion, sigh.

On Jul 12, 2004, at 11:28 AM, Peter Dimov wrote:
Howard Hinnant wrote:
On Jul 11, 2004, at 8:46 AM, Peter Dimov wrote:
[...]
Mutex * mutex() const; // returns: the associated mutex;
Why return a pointer instead of a reference? What about this instead?
Mutex& mutex(); const Mutex& mutex() const;
Because of the use case shown below:
void f( scoped_lock & lock ) { // check for lock validity assert( lock.locked() && lock.mutex() == &my_mutex );
// proceed with operation }
A pointer makes it clear that it is the identity of the associated mutex that is being queried. A (const-"correct") reference return implies that the purpose of the accessor is to return, well, a reference to the mutex object, presumably so that the client can do something with it.
That would put to rest any questions about mutex() transferring mutex ownership, e.g.:
delete lock.mutex();
I considered
bool is_associated_with( Mutex const & m ) const;
but that's a bit too much of a handholding for my taste, and doesn't allow us to use mutex() in the Effects clauses. The kind of a person that would delete lock.mutex() would never get a multithreaded program correct anyway.
If we expose either a Mutex* or a Mutex&, and we standardize a public Mutex interface (with lock(), unlock(), etc.), then we are saying that one can call functions on the pointer or reference returned by mutex(). And now that I write that sentence my skin is beginning to crawl. ;-) I have occasionally seen a need for using a mutex outside of a scoped_lock, when the lock/unlock pattern is not as neat as what scoped_lock provides. Alexander's read/write lock algorithm is a good example: http://groups.google.com/groups?selm=3B166244.F923B993%40web.de . And so I do not object to standardizing a public mutex interface so that they can be used outside of a scoped_lock. But allowing a mutex to be manipulated within a scoped_lock seems dangerous. Brainstorming: template <class Mutex> bool operator==(const scoped_lock<Mutex>&, const Mutex&); template <class Mutex> bool operator==(const Mutex&, const scoped_lock<Mutex>&); instead of Mutex* scoped_lock::mutex() const; ? Might look like: void f( scoped_lock & lock ) { // check for lock validity assert( lock.locked() && lock == my_mutex ); // proceed with operation } Haven't even prototyped it, so I'm not even sure it will work. Just a thought.
Now that we got rid of the excess locks, how about doing the same with the mutexes?
That worries me a lot. We need several different flavors of mutex because the more functionality you put into a mutex, the more expensive it is both in terms of size and speed. Why pay for a recursive mutex if you don't need one? Why pay for a timed mutex? But when you need one of these more expensive types, then the price is worth it.
First, please note that I never said anything about mutex being recursive (and the specification says that relocking is undefined behavior). A recursive mutex is a separate entity (and its specification is slightly different).
That aside. It is precisely the assertion that (1) a try_mutex is more expensive than a mutex, and (2) that a timed_mutex is more expensive than a try_mutex, that I am challenging. I see no evidence for (1). (2) is more interesting. You will note that POSIX indeed makes you explicitly state whether you want a recursive mutex or not - because recursive mutexes are more expensive. However no such requirement exists for "timed mutexes". Either all mutexes are timed, or they are not. This, for me, indicates that a timed mutex is not more expensive than a try mutex, just that pthread_mutex_timedlock is a late addition to the standard.
Do you have data on whether your platforms support pthread_mutex_timedlock?
I have one platform to support where I preferred a different implementation for mutex and try_mutex: Win98. Admittedly that one is quickly going obsolete. Nevertheless I currently still need to support it. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ dllproc/base/tryentercriticalsection.asp
TryEnterCriticalSection
Requires Windows XP, Windows 2000 Professional, or Windows NT Workstation 4.0.
Mac OS X up through 10.3.4 (most current) does not appear to support pthread_mutex_timedlock, and so on this platform my mutex implementation differs from my timed_mutex implementation.
I can't speak for the boost implementation (haven't carefully studied it), but the Metrowerks implementation also supports conditions operating on a recursive mutex.
Interesting. I presume that this was a deliberate design decision. There is a school of thought that says that a recursive mutex can only be used to sweep bugs under the carpet and is never needed in a correctly designed multithreaded program, which probably applies doubly to conditions operating on a recursive mutex locked more than once. But I'm not qualified to judge.
I have heard that school of thought too. It sounds somewhat like the same argument I've heard against goto. I agree with neither, but admittedly am more familiar with goto than I am with recursive mutexes. I have code where I use a recursive mutex. I wrote it knowing the bad karma surrounding recursive mutexes, but I used it anyway. I am sure that code could be restructured to use a non-recursive mutex. But the recursive mutex was both convenient for me, and provided correct and reasonable code in this instance (the code can not be considered buggy by any black box test I'm aware of). Maybe some day that code will bubble up my priority list and I'll rewrite it to use a non-recursive mutex. But that would be a significant rewrite, and I'm grateful that I am not forced into that decision by my threads library. -Howard

Howard Hinnant wrote:
On Jul 12, 2004, at 11:28 AM, Peter Dimov wrote:
Howard Hinnant wrote:
On Jul 11, 2004, at 8:46 AM, Peter Dimov wrote:
[...]
Mutex * mutex() const; // returns: the associated mutex;
Why return a pointer instead of a reference? What about this instead?
Mutex& mutex(); const Mutex& mutex() const;
Because of the use case shown below:
void f( scoped_lock & lock ) { // check for lock validity assert( lock.locked() && lock.mutex() == &my_mutex );
// proceed with operation }
A pointer makes it clear that it is the identity of the associated mutex that is being queried. A (const-"correct") reference return implies that the purpose of the accessor is to return, well, a reference to the mutex object, presumably so that the client can do something with it.
That would put to rest any questions about mutex() transferring mutex ownership, e.g.:
delete lock.mutex();
I considered
bool is_associated_with( Mutex const & m ) const;
but that's a bit too much of a handholding for my taste, and doesn't allow us to use mutex() in the Effects clauses. The kind of a person that would delete lock.mutex() would never get a multithreaded program correct anyway.
If we expose either a Mutex* or a Mutex&, and we standardize a public Mutex interface (with lock(), unlock(), etc.), then we are saying that one can call functions on the pointer or reference returned by mutex(). And now that I write that sentence my skin is beginning to crawl. ;-)
I have occasionally seen a need for using a mutex outside of a scoped_lock, when the lock/unlock pattern is not as neat as what scoped_lock provides. Alexander's read/write lock algorithm is a good example: http://groups.google.com/groups?selm=3B166244.F923B993%40web.de . And so I do not object to standardizing a public mutex interface so that they can be used outside of a scoped_lock. But allowing a mutex to be manipulated within a scoped_lock seems dangerous.
Brainstorming:
template <class Mutex> bool operator==(const scoped_lock<Mutex>&, const Mutex&);
template <class Mutex> bool operator==(const Mutex&, const scoped_lock<Mutex>&);
instead of Mutex* scoped_lock::mutex() const;
?
Might look like:
void f( scoped_lock & lock ) { // check for lock validity assert( lock.locked() && lock == my_mutex );
// proceed with operation }
Haven't even prototyped it, so I'm not even sure it will work. Just a thought.
If you don't want people messing with the mutex through the scoped_lock, use a void* disguised as a typedef: typedef void const* mutex_id_type; struct mutex { mutex_id_type id() const { return this; } ... }; template< class Mutex > { Mutex & m_; mutex_id_type mutex_id() const { return m_.id(); } }; Usage might look like: void f( scoped_lock & lock ) { // check for lock validity assert( lock.locked() && lock.mutex_id() == my_mutex.id() ); // proceed with operation } -- Eric Niebler Boost Consulting www.boost-consulting.com

Howard Hinnant <hinnant@twcny.rr.com> writes:
but that's a bit too much of a handholding for my taste, and doesn't allow us to use mutex() in the Effects clauses. The kind of a person that would delete lock.mutex() would never get a multithreaded program correct anyway.
If we expose either a Mutex* or a Mutex&, and we standardize a public Mutex interface (with lock(), unlock(), etc.), then we are saying that one can call functions on the pointer or reference returned by mutex(). And now that I write that sentence my skin is beginning to crawl. ;-)
Why not just build a mutex identity object that wraps a pointer? -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

Howard Hinnant wrote:
On Jul 12, 2004, at 11:28 AM, Peter Dimov wrote:
[...]
If we expose either a Mutex* or a Mutex&, and we standardize a public Mutex interface (with lock(), unlock(), etc.), then we are saying that one can call functions on the pointer or reference returned by mutex(). And now that I write that sentence my skin is beginning to crawl. ;-)
This is what 'class mutex' looks like in my original message: class mutex // DefaultConstructible { private: void lock(); bool try_lock(); bool timed_lock( xtime xt ); void unlock(); }; IOW, the function names are standardized, but they aren't public. The intent is to allow users to write a mutex class M that can be used with std::tr2::lock<M>. As a mutex doesn't have a public interface, the mutex() accessor cannot be used to manipulate a lock's associated mutex. It is, of course, possible to make this interface public, but I haven't proposed that. ;-) [...]
Brainstorming:
template <class Mutex> bool operator==(const scoped_lock<Mutex>&, const Mutex&);
template <class Mutex> bool operator==(const Mutex&, const scoped_lock<Mutex>&);
instead of Mutex* scoped_lock::mutex() const;
?
That's just my 'is_associated_with' with a different name. It's a bit more limited than mutex(); for example, you can't use a map<void*, ...> keyed on mutex(), and you can't compare l1.mutex() and l2.mutex() (using std::less<void*>) to derive a locking order.
Do you have data on whether your platforms support pthread_mutex_timedlock?
I have one platform to support where I preferred a different implementation for mutex and try_mutex: Win98. Admittedly that one is quickly going obsolete. Nevertheless I currently still need to support it.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ dllproc/base/tryentercriticalsection.asp
TryEnterCriticalSection
Requires Windows XP, Windows 2000 Professional, or Windows NT Workstation 4.0.
Have you seen Alexander's 'swap-based' implementation? The link was in my original post, right after "It is true that Windows 95 does not have TryEnterCriticalSection, but an implementation can use Alexander Terekhov's alternative:" http://lists.boost.org/MailArchives/boost/msg64648.php
Mac OS X up through 10.3.4 (most current) does not appear to support pthread_mutex_timedlock, and so on this platform my mutex implementation differs from my timed_mutex implementation.
Not good. This means that you cannot be persuaded to abandon timed_mutex, which in turn means that the future standard will probably have a timed_mutex, even if at that future time Mac OS X does have pthread_mutex_timedlock. The alternative reality would have a mutex with timed_lock #ifdef'ed on _POSIX_TIMEOUTS until conformance arrives.

On Jul 12, 2004, at 4:39 PM, Peter Dimov wrote:
This is what 'class mutex' looks like in my original message:
class mutex // DefaultConstructible { private:
void lock(); bool try_lock(); bool timed_lock( xtime xt ); void unlock(); };
IOW, the function names are standardized, but they aren't public. The intent is to allow users to write a mutex class M that can be used with std::tr2::lock<M>.
As a mutex doesn't have a public interface, the mutex() accessor cannot be used to manipulate a lock's associated mutex.
It is, of course, possible to make this interface public, but I haven't proposed that. ;-)
Sorry, I missed the "private" declaration. And I like the idea of standardizing the interface for the purpose you state. I'd be tempted to make them public so that they could be used outside of a lock, or with user-written locks. (e.g. someone writes a read/write lock that is better than what we're dreaming of). We can still standardize an interface, but not require the entire interface. E.g. here's what you must have: void lock(); void unlock(). And if you want to try-lock, you have to also provide: bool try_lock(); And if you want to provide timed-locks... And if you want to provide read-lock... And if you want to provide write-lock... And if you want to provide upgradable-read-lock... Personally I'm a little giddy over the try-timed-upgradable-read-lock, but hey, it could really be useful in just the right circumstance. But I wouldn't want to pay for that if I just needed a simple lock. Who knows, there might even be an application for a read (but not write) lock? Everybody can concurrently access this structure, but you must "register" yourself first by obtaining this read-lock... <shrug> no practical application in mind, just brainstorming. And then there's recursion, same interface but slightly different functionality... maybe: class Im_a_recursive_mutex { public: typedef int recursive; ... }; ? Nested types can be detected at compile time, so clients (locks or whatever) can figure out what they're dealing with (and a helpful utility traits provided to do this). We provide an extendable mutex concept that can plug & play with an extendable lock concept, provide a few example mutexes and locks, and turn the world loose on it to continue the work?
[...]
Brainstorming:
template <class Mutex> bool operator==(const scoped_lock<Mutex>&, const Mutex&);
template <class Mutex> bool operator==(const Mutex&, const scoped_lock<Mutex>&);
instead of Mutex* scoped_lock::mutex() const;
?
That's just my 'is_associated_with' with a different name.
Yeah, but with prettier spelling. ;-)
It's a bit more limited than mutex(); for example, you can't use a map<void*, ...> keyed on mutex(), and you can't compare l1.mutex() and l2.mutex() (using std::less<void*>) to derive a locking order.
<nod> good points. Perhaps all 6 relationals should be added to the (public) basic mutex interface? In C++0X if containers are move-aware and mutexes movable, then you could then map<mutex, ...>.
Have you seen Alexander's 'swap-based' implementation? The link was in my original post, right after "It is true that Windows 95 does not have TryEnterCriticalSection, but an implementation can use Alexander Terekhov's alternative:"
<nod> I truly appreciate Alexander's continued willingness to publish algorithms such as this. Thanks Alexander!
Mac OS X up through 10.3.4 (most current) does not appear to support pthread_mutex_timedlock, and so on this platform my mutex implementation differs from my timed_mutex implementation.
Not good. This means that you cannot be persuaded to abandon timed_mutex, which in turn means that the future standard will probably have a timed_mutex, even if at that future time Mac OS X does have pthread_mutex_timedlock.
The alternative reality would have a mutex with timed_lock #ifdef'ed on _POSIX_TIMEOUTS until conformance arrives.
<sigh> It isn't just Mac OS X either. There are other (embedded) platforms I've neglected to mention because I'm not positive exactly what my NDA is. But it can get pretty ugly out there in some cases. If OS's would just adopt the pthreads knowledge and experience the world would be such a better place. Multithreading is a tough area and OS designers aren't necessarily experts in that particular area (and I'm not claiming to be either). And without the proper OS functionality, add-on threading libraries are inherently limited. ... No point I'm trying to make. Just being grumpy. ;-) -Howard

Howard Hinnant wrote:
On Jul 12, 2004, at 4:39 PM, Peter Dimov wrote:
The alternative reality would have a mutex with timed_lock #ifdef'ed on _POSIX_TIMEOUTS until conformance arrives.
<sigh> It isn't just Mac OS X either. There are other (embedded) platforms I've neglected to mention because I'm not positive exactly what my NDA is. But it can get pretty ugly out there in some cases. If OS's would just adopt the pthreads knowledge and experience the world would be such a better place. Multithreading is a tough area and OS designers aren't necessarily experts in that particular area (and I'm not claiming to be either). And without the proper OS functionality, add-on threading libraries are inherently limited. ... No point I'm trying to make. Just being grumpy. ;-)
I understand. However in this particular case I'm not sure that you're doing your users a favor. Let's say I'm a library/application developer that can take advantage of timed_lock to provide some enhancements as a QoI, but at the same time I most definitely don't want to sacrifice performance; i.e. performance is a priority that far outweighs the optional enhancements provided by timed_lock. So I'd prefer to use mutex::timed_lock, getting a compile-time error on platforms that do not support an overhead-free "timed mutex". If, on the other hand, the performance loss (~3-5x, I've read) is not a concern, I can simply emulate the timed mutex myself; your emulation is no better than mine.

On Jul 13, 2004, at 7:39 AM, Peter Dimov wrote:
I understand. However in this particular case I'm not sure that you're doing your users a favor. Let's say I'm a library/application developer that can take advantage of timed_lock to provide some enhancements as a QoI, but at the same time I most definitely don't want to sacrifice performance; i.e. performance is a priority that far outweighs the optional enhancements provided by timed_lock. So I'd prefer to use mutex::timed_lock, getting a compile-time error on platforms that do not support an overhead-free "timed mutex".
If, on the other hand, the performance loss (~3-5x, I've read) is not a concern, I can simply emulate the timed mutex myself; your emulation is no better than mine.
How do we serve the client who needs a timed mutex, on every platform, and would rather not implement it himself? If mutex::timed_lock sometimes compiles and sometimes doesn't, then that isn't the answer. And telling the client that he can just emulate this functionality himself is no different than telling him he can just emulate the STL himself. The emulation on platforms with non-native support requires a certain amount of expertise, careful coding, and most importantly, time. Must everyone who must have a timed mutex become an implementor of it? I don't have a problem if mutex is allowed to optionally sport a timed_lock() member function (presumably only if it could do so with "acceptable" cost). I just don't want to require it to (in case the costs are deemed "unacceptable"). I'd rather there exist a timed_mutex class with that requirement. Would this not satisfy both of our hypothetical clients? -Howard

Howard Hinnant wrote:
How do we serve the client who needs a timed mutex, on every platform, and would rather not implement it himself? If mutex::timed_lock sometimes compiles and sometimes doesn't, then that isn't the answer. And telling the client that he can just emulate this functionality himself is no different than telling him he can just emulate the STL himself. The emulation on platforms with non-native support requires a certain amount of expertise, careful coding, and most importantly, time. Must everyone who must have a timed mutex become an implementor of it?
Probably not, _if_ this hypothetical client exists. I don't doubt it that once you provide a timed_mutex, many will find it useful, with or without quotes. But the mutex+condition underneath will probably mean that these users are not getting their money's worth from the design. If they explicitly see the monitor pattern hidden under the opaque timed_mutex, they might be able to put it to better use.
I don't have a problem if mutex is allowed to optionally sport a timed_lock() member function (presumably only if it could do so with "acceptable" cost). I just don't want to require it to (in case the costs are deemed "unacceptable").
I want the standard to require mutex::timed_lock, because if there is no such requirement, no implementation will provide it. Why bother? No points are taken off conformance tests, and the users can't really demand it, because it's not required.
I'd rather there exist a timed_mutex class with that requirement. Would this not satisfy both of our hypothetical clients?
Yes, having both mutex and timed_mutex will satisfy our hypotheticals. However if mutex::timed_lock is required, timed_mutex makes no sense from specification point of view. Anyway, (mutex with try_lock and optional timed_lock + timed_mutex) * 2 + static_mutex would still be better than (mutex + try_mutex + timed_mutex) * 2.

On Jul 13, 2004, at 12:06 PM, Peter Dimov wrote:
Yes, having both mutex and timed_mutex will satisfy our hypotheticals. However if mutex::timed_lock is required, timed_mutex makes no sense from specification point of view.
My bad. There are at least 3 important clients to consider: 1. Give me mutex::timed_lock(), but only if it doesn't cost much, else I want a compile time error. 2. Give me timed_mutex::timed_lock(). Gotta have it on every platform. 3. Give me mutex::lock(), and please make it as small and fast as possible. I'm putting this mutex into my objects, and I'll have container<my_object>, so minimal overhead in the mutex is important (I don't care about timed_lock(), never use it). Did I miss any? Have I enumerated any "clients" that anyone thinks don't exist, or exist in vanishingly small numbers? Personally I feel that 2 and 3 definitely exist in significant numbers. I'm not positive about 1, but readily admit that I wouldn't want to bet much that 1 exists in vanishingly small numbers.
I want the standard to require mutex::timed_lock, because if there is no such requirement, no implementation will provide it. Why bother? No points are taken off conformance tests, and the users can't really demand it, because it's not required.
I think you grossly understate the motivation of all the std::lib vendors I'm aware of today. While the Metrowerks implementation is quite clearly the best one available, ;-) all implementations are aggressively pursuing conformance, optimizations and extensions. I'm not aware of any mediocre implementations of the std::lib today (compared with say 7 years ago). And I'm also not aware of any std::lib vendor that isn't listening very closely to their customers. My compliments to my competitors. If an optional mutex::timed_lock() is a good idea, and if C++0X blesses the idea, and if a few customers of a specific vendor request it for a specific platform where it would indeed make sense, I can practically guarantee you it will be implemented. If on the other hand, mutex::timed_lock() is required, I can guarantee you that customer #3, on some platforms, is going to be a very unhappy camper. Unhappy enough to not use this library, and thus we will have failed. -Howard

Howard Hinnant wrote:
On Jul 13, 2004, at 12:06 PM, Peter Dimov wrote:
Yes, having both mutex and timed_mutex will satisfy our hypotheticals. However if mutex::timed_lock is required, timed_mutex makes no sense from specification point of view.
My bad. There are at least 3 important clients to consider:
1. Give me mutex::timed_lock(), but only if it doesn't cost much, else I want a compile time error.
2. Give me timed_mutex::timed_lock(). Gotta have it on every platform.
3. Give me mutex::lock(), and please make it as small and fast as possible. I'm putting this mutex into my objects, and I'll have container<my_object>, so minimal overhead in the mutex is important (I don't care about timed_lock(), never use it).
Almost. Client 1 is actually: 1a. Give me mutex::timed_lock that (if it) doesn't have a cost; that is, it is as fast as mutex::lock in the non-blocking case, and it doesn't impose performance penalties on mutex::lock, mutex::try_lock, mutex::unlock. This is effectively the same as POSIX. OK, it may be "almost doesn't have a cost", or "doesn't cost much", but the intent is clear. In a perfect world, all platforms will have such a timed_lock, and all clients would be happy. And our specification should not be suboptimal in this world. OTOH I appreciate your arguments. Even though pthread_mutex_timedlock may be implementable everywhere, it's a fact of life that some platforms do not, and will not, supply it. However if a platform has a timed lock that satisfies (1a) then it should be exposed as mutex::timed_lock. I don't know whether there's such a precedent in the standard library; nothing optional comes to mind. A problem with timed_mutex is that once it gets into the standard, it can't be removed, even if it turns out that type (2) clients are few, or that having a timed_mutex handy doesn't encourage careful design. Whereas the reverse situation - no std::tr2::timed_mutex and customer revolt - is easily fixable; you just provide it as an extension (something that Boost.Threads will also need to do, BTW, even if we redesign the mutexes and the locks). Since the interface is already specified by std::tr2::mutex, a hash_map situation should not occur. ;-) Hm. Where were we. Oh yes. - mutex::timed_lock: optional. - timed_mutex: in the std or not? - recursive_mutex: in the std? Seems so. - condition::wait with recursive_mutex: compiles? - condition::wait with recursive_mutex locked more than once: defined? if so, how?

On Jul 13, 2004, at 3:59 PM, Peter Dimov wrote:
Hm. Where were we.
What?! I thought you were holding the map! <quickly looks for road signs>. OMG we're lost!
Oh yes.
<phew!> :-)
- mutex::timed_lock: optional.
<nod>
- timed_mutex: in the std or not?
well, in boost anyway with the intent of proposing for std, I vote yes.
- recursive_mutex: in the std? Seems so.
<nod>
- condition::wait with recursive_mutex: compiles?
<nod>, but I could be persuaded otherwise if we can detect a recursive mutex at compile time.
- condition::wait with recursive_mutex locked more than once: defined? if so, how?
This is the big question in my mind right now. I know how to make it work with a home-grown recursive mutex. But I don't know how to make it work with a native recursive mutex, combined with a native condition. I'm somewhat surprised that it doesn't just work. Alexander, can we send a bug report to the pthreads guys? Or would we just be laughed out of the room? I'm not educated enough to know what the problems are to making it work. And clearly it doesn't work today, so it's a problem whether or not it can be fixed. <sigh> That probably backs us into undefined behavior if the mutex is recursively locked, or refusing to compile at all with a recursive mutex. I'm unsure at this point which would be the better option. -Howard

Howard Hinnant wrote: [... cv.wait with recursively locked mutex ...]
This is the big question in my mind right now. I know how to make it work with a home-grown recursive mutex. But I don't know how to make it work with a native recursive mutex, combined with a native condition. I'm somewhat surprised that it doesn't just work.
It does just work. As long as you call cv.wait with a recursive mutex locked by the calling threads and its "lock count == 1". Calling it with "lock count != 1" is a bug (violation of a precondition in C++ terms).
Alexander, can we send a bug report to the pthreads guys?
http://www.opengroup.org/austin/defectform.html
Or would we just be laughed out of the room? I'm not educated enough to know what the problems are to making it work.
It is a philosophical issue, not technical. I wouldn't oppose if you would propose to add int pthread_mutex_setlockcount( // must be locked by the calling thread pthread_mutex_t * mutex, // must be positive (> 1 can be set on PTHREAD_MUTEX_RECURSIVE only) unsigned lockcount, // can be null unsigned * oldlockcount ); (or something like that, "get" aside for a moment) so that you could do [...] while (!predicate) { unsigned lockcount; pthread_mutex_setlockcount(&mutex, 1, &lockcount); pthread_cond_wait(&condvar, &mutex); pthread_mutex_setlockcount(&mutex, lockcount, 0); } [...] if/when you're ablsolutely 100% sure that what you're doing is OK.
And clearly it doesn't work today, so it's a problem whether or not it can be fixed. <sigh> That probably backs us into undefined behavior if the mutex is recursively locked,
Exactly. "may fail error" (in POSIX terms; optional "throws" that end up in std::unexpected with no return from the "failed" call). http://groups.google.com/groups?selm=3434E6BA.ABD%40zko.dec.com ---- The one omission in XSH5 recursive mutex support was a standard error code for the programming ERROR of trying to wait on a condition with a recursively locked mutex. If it's safe to release the recursive locks, the code should have done so. If it's not safe to release, then the runtime MUST NOT do so. ----
or refusing to compile at all with a recursive mutex.
No, that's not good. regards, alexander.

On Jul 14, 2004, at 6:27 AM, Alexander Terekhov wrote:
It is a philosophical issue, not technical.
Thanks for your comments. Speaking philosophically ... just wondering out loud, not trying to solve anything, I wonder if this doesn't boil down to an object oriented view vs a functional view. Because the few times I've felt the need for a recursive mutex is when I had an object, which contained a single mutex, which had several public member functions that just happened to call each other as an implementation detail. Something like: class A { public: void foo1(); // calls foo2 and foo3 under the covers void foo2(); void foo3(); private: mutex mut_; }; All three functions lock the mutex to synchronize access so A can maintain its invariant. But foo1() is an especially complicated operation and to help it do it's job also calls foo2() and foo3() under the covers. foo1() can't afford to just let foo2() and foo3() handle the locking because it also breaks invariants within it's own body and/or must execute foo2() and foo3() atomically. foo1 may restore invariants before calling foo2 and foo3, or foo2 and foo3 may be smart enough to deal with the state in which foo1 calls them (just depends on the application). So although there are non-recursive ways to solve this problem, simply allowing mut_ to be locked recursively is convenient. The alternative is write a private function that might look like: void do_foo(int parm); where parm indicates whether you need to do just 2, just 3, or both 2 and 3, and locks accordingly. Now foo1, foo2 and foo3 can all forward to do_foo without locking at the top level. The more interdependent foo's you have though, the more convenient the recursive approach looks. Admittedly, I've never felt the need to wait on a recursively locked mutex, but it seems like it would be reasonable for foo2() in the above example to do so, whether or not it had been called from an external client, or from foo1(). foo1() could tell foo2() that it had been called from foo1() so that foo2() would know to do an extra unlock/lock before/after the wait. But with many foo's that solution could get as complicated, or even more so, than the non-recursive do_foo() approach. <sigh> Hope I never feel the need to wait on a recursive mutex! :-) -Howard

Howard Hinnant wrote:
On Jul 14, 2004, at 6:27 AM, Alexander Terekhov wrote:
It is a philosophical issue, not technical.
Thanks for your comments.
Speaking philosophically ... just wondering out loud, not trying to solve anything, I wonder if this doesn't boil down to an object oriented view vs a functional view. Because the few times I've felt the need for a recursive mutex is when I had an object, which contained a single mutex, which had several public member functions that just happened to call each other as an implementation detail. Something like:
class A { public: void foo1(); // calls foo2 and foo3 under the covers void foo2(); void foo3(); private: mutex mut_; };
Yep, classic. ;-) The solution is, of course: class A { public: void foo1() { scoped_lock<> lock(mut_); foo1_impl(); } void foo2() { scoped_lock<> lock(mut_); foo2_impl(); } void foo3() { scoped_lock<> lock(mut_); foo3_impl(); } private: void foo1_impl(); // calls foo2_impl & foo3_impl void foo2_impl(); void foo3_impl(); mutex mut_; }; I have a recursive_mutex use case that's not as trivial, though. Imagine a mutex pool with a fixed size N that protects an unbounded set of objects (hashing the object address mod N to determine the mutex to lock). Now when you need to protect two objects at the same time, due to the probability of a collision the mutexes need to be recursive. It's still not watertight, because if the two locks are removed from each other, this seems like a deadlock invitation; and if the two locks are made at the same time, the recursive lock can be avoided locally. [...]
Admittedly, I've never felt the need to wait on a recursively locked mutex, but it seems like it would be reasonable for foo2() in the above example to do so, whether or not it had been called from an external client, or from foo1().
That's exactly the problem. If foo2 waits, it will release not only its own lock, but also foo1's lock, which may not be safe to release (i.e. foo1 may have left the object in a broken state, expecting that nobody can see it since it holds a lock.) It's all in Butenhof's posts.

"Peter Dimov" <pdimov@mmltd.net> writes:
I have a recursive_mutex use case that's not as trivial, though. Imagine a mutex pool with a fixed size N that protects an unbounded set of objects (hashing the object address mod N to determine the mutex to lock). Now when you need to protect two objects at the same time, due to the probability of a collision the mutexes need to be recursive. It's still not watertight, because if the two locks are removed from each other, this seems like a deadlock invitation; and if the two locks are made at the same time, the recursive lock can be avoided locally.
Can't you just check the hashes and only lock once in that case? -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

On Wed, 14 Jul 2004 15:27:08 -0400, David Abrahams <dave@boost-consulting.com> wrote:
"Peter Dimov" <pdimov@mmltd.net> writes:
I have a recursive_mutex use case that's not as trivial, though. Imagine a mutex pool with a fixed size N that protects an unbounded set of objects (hashing the object address mod N to determine the mutex to lock). Now when you need to protect two objects at the same time, due to the probability of a collision the mutexes need to be recursive. It's still not watertight, because if the two locks are removed from each other, this seems like a deadlock invitation; and if the two locks are made at the same time, the recursive lock can be avoided locally.
Can't you just check the hashes and only lock once in that case?
AFAICT, you'd have to record the objects to which the specific thread had the lock otherwise you can't discriminate on who owns the lock. You could do this either as TSS for which object or, alternatively, via an intrusive which_thread struct (bit field for # of threads?) in the object I guess. I don't have a good solution to this. The way I approach it is just to write code very carefully to avoid the deadlock, but I still get bitten from time to time. On the flip side, if I have a lock per object in a large collection, on win32 I get tens or hundreds of thousands of kernel objects, this made me uncomfortable so I went to a vector of mutexes for this puppy but only as it made me uncomfortable. Sure this was a bad thing for a win9X, but I have not been able to dig up, or see, any problems with doing this on win32 (i.e. it works) apart from the fact that it looks strange and no-one else seems to do it, which is enough to make me nervous. Any advice here? $AUD 0.02, Matt Hurd.

Matt Hurd <matt.hurd@gmail.com> writes:
On Wed, 14 Jul 2004 15:27:08 -0400, David Abrahams <dave@boost-consulting.com> wrote:
"Peter Dimov" <pdimov@mmltd.net> writes:
I have a recursive_mutex use case that's not as trivial, though. Imagine a mutex pool with a fixed size N that protects an unbounded set of objects (hashing the object address mod N to determine the mutex to lock). Now when you need to protect two objects at the same time, due to the probability of a collision the mutexes need to be recursive. It's still not watertight, because if the two locks are removed from each other, this seems like a deadlock invitation; and if the two locks are made at the same time, the recursive lock can be avoided locally.
Can't you just check the hashes and only lock once in that case?
AFAICT, you'd have to record the objects to which the specific thread had the lock otherwise you can't discriminate on who owns the lock.
That's only true if a given thread can lock more than two objects at the same time. Isn't that likely to lead to deadlock, regardless? If each thread is only limited to locking one pair you can lock the mutex for the first object, check the 2nd one to see if it's got the same hash, and if not, lock the 2nd object's mutex. I'm pretty sure, anyway ;-) -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote:
Matt Hurd <matt.hurd@gmail.com> writes:
On Wed, 14 Jul 2004 15:27:08 -0400, David Abrahams <dave@boost-consulting.com> wrote:
"Peter Dimov" <pdimov@mmltd.net> writes:
I have a recursive_mutex use case that's not as trivial, though. Imagine a mutex pool with a fixed size N that protects an unbounded set of objects (hashing the object address mod N to determine the mutex to lock). Now when you need to protect two objects at the same time, due to the probability of a collision the mutexes need to be recursive. It's still not watertight, because if the two locks are removed from each other, this seems like a deadlock invitation; and if the two locks are made at the same time, the recursive lock can be avoided locally.
Can't you just check the hashes and only lock once in that case?
AFAICT, you'd have to record the objects to which the specific thread had the lock otherwise you can't discriminate on who owns the lock.
That's only true if a given thread can lock more than two objects at the same time. Isn't that likely to lead to deadlock, regardless?
Yep, have you read the last sentence in the block you quoted? Anyway: consider this (oversimplified) situation: // f.cpp static object o; void f() { lock( &o ); // do things unlock( &o ); } // g.cpp void f(); static object o2; void g() { lock( &o2 ); f(); unlock( &o2 ); } Since the f() lock is always nested within the g() lock, there can be no deadlock.

"Peter Dimov" <pdimov@mmltd.net> writes:
David Abrahams wrote:
Matt Hurd <matt.hurd@gmail.com> writes:
On Wed, 14 Jul 2004 15:27:08 -0400, David Abrahams <dave@boost-consulting.com> wrote:
"Peter Dimov" <pdimov@mmltd.net> writes:
Can't you just check the hashes and only lock once in that case?
AFAICT, you'd have to record the objects to which the specific thread had the lock otherwise you can't discriminate on who owns the lock.
That's only true if a given thread can lock more than two objects at the same time. Isn't that likely to lead to deadlock, regardless?
Yep, have you read the last sentence in the block you quoted?
Yes, but I don't see how it's relevant.
Anyway: consider this (oversimplified) situation:
// f.cpp
static object o;
void f() { lock( &o ); // do things unlock( &o ); }
// g.cpp
void f();
static object o2;
void g() { lock( &o2 ); f(); unlock( &o2 ); }
Since the f() lock is always nested within the g() lock, there can be no deadlock.
Sorry, I don't see more than two objects locked at the same time here. No big deal, I'm probably off base here so will relurk. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote:
"Peter Dimov" <pdimov@mmltd.net> writes:
I have a recursive_mutex use case that's not as trivial, though. Imagine a mutex pool with a fixed size N that protects an unbounded set of objects (hashing the object address mod N to determine the mutex to lock). Now when you need to protect two objects at the same time, due to the probability of a collision the mutexes need to be recursive. It's still not watertight, because if the two locks are removed from each other, this seems like a deadlock invitation; and if the two locks are made at the same time, the recursive lock can be avoided locally.
Can't you just check the hashes and only lock once in that case?
You can, if the two locks are made at the same time, but not if one is "deeper" in the call hierarchy.

On Wed, 14 Jul 2004 18:45:00 +0300, Peter Dimov <pdimov@mmltd.net> wrote:
Howard Hinnant wrote:
On Jul 14, 2004, at 6:27 AM, Alexander Terekhov wrote:
It is a philosophical issue, not technical.
Thanks for your comments.
Speaking philosophically ... just wondering out loud, not trying to solve anything, I wonder if this doesn't boil down to an object oriented view vs a functional view. Because the few times I've felt the need for a recursive mutex is when I had an object, which contained a single mutex, which had several public member functions that just happened to call each other as an implementation detail. Something like:
class A { public: void foo1(); // calls foo2 and foo3 under the covers void foo2(); void foo3(); private: mutex mut_; };
Yep, classic. ;-)
The solution is, of course:
class A { public:
void foo1() { scoped_lock<> lock(mut_); foo1_impl(); }
void foo2() { scoped_lock<> lock(mut_); foo2_impl(); }
void foo3() { scoped_lock<> lock(mut_); foo3_impl(); }
private:
void foo1_impl(); // calls foo2_impl & foo3_impl void foo2_impl(); void foo3_impl();
mutex mut_; };
Yes, that is the classic solution but it never strikes me as quite what I need. I've been struggling to find a cute solution I'm happy with. Mainly because I also typically want to expose the foo1_impl() for the cases when I wish to lock externally for multiple operations. One thing I've done is have the standard interface lock and have template methods that take true and false for locking and non-locking. do_this(); // locks - safety first do_this<true>(); // locks do_this<false>(); // no locking To do this I inherit a monitor class which exposes a guard() method that return a mutex reference which is used for locking. The messy part is being able to do it without lots of boilerplate code. Presently I use a macro for getter/setter attributes which implements the appropriate methods and also uses a compile time check for attribute size to determine if locking is truly necessary for the platform (some operations are atomic and need no locking, e.g. 32 bit ops on IA32 and 64 bit ops on IA64). This is fine. However this macro/specialisation approach falls apart when you have an arbitrary method as I don't know a neat way to encapsulate the passing of the parameters in the macro for replication. However, the named_params library does this nicely and I think I could use that for the arbitary method approach, I just can't quite find the time to do it. I've previously posted this stuff but can again if there is any interest. Regards, Matt Hurd. _______________ Here is a simple attribute only example: namespace te { template < class S = synch::shareable
class instrument : public synch::monitor<S> { SYNCH_SIMPLE_ATTRIBUTE( susquehanna_code, std::string ); SYNCH_SIMPLE_ATTRIBUTE( exchange_code, std::string ); SYNCH_SIMPLE_ATTRIBUTE( underlying, std::string ); SYNCH_SIMPLE_ATTRIBUTE( bid, double ); SYNCH_SIMPLE_ATTRIBUTE( bid_volume, double ); SYNCH_SIMPLE_ATTRIBUTE( ask, double ); SYNCH_SIMPLE_ATTRIBUTE( ask_volume, double ); SYNCH_SIMPLE_ATTRIBUTE( last, double ); SYNCH_SIMPLE_ATTRIBUTE( last_volume, double ); SYNCH_SIMPLE_ATTRIBUTE( prior_settlement, double ); public: instrument() : synch::monitor<S>() { } template < class T > instrument( const T & t) : synch::monitor<S>() { T::lock lk( t.guard(), synch::lock_status::shared ); susquehanna_code<false> ( t.susquehanna_code<false>() ); exchange_code<false> ( t.exchange_code<false>() ); underlying<false> ( t.underlying<false>() ); bid<false> ( t.bid<false>() ); bid_volume<false> ( t.bid_volume<false>() ); ask<false> ( t.ask<false>() ); ask_volume<false> ( t.ask_volume<false>() ); last<false> ( t.last<false>() ); last_volume<false> ( t.last_volume<false>() ); prior_settlement<false> ( t.prior_settlement<false>() ); } template< class T > instrument< S > & operator=( const T& rhs ) { if (&rhs != this) { lock lk_this( guard(), synch::lock_status::exclusive ); instrument<S> temp(rhs); swap(temp); } return *this; } template< class T> void swap( T& source) throw() { std::swap( susquehanna_code_, source.susquehanna_code_); std::swap( exchange_code_, source.exchange_code_); std::swap( underlying_, source.underlying_); std::swap( bid_, source.bid_); std::swap( bid_volume_, source.bid_volume_); std::swap( ask_, source.ask_); std::swap( ask_volume_, source.ask_volume_); std::swap( last_, source.last_); std::swap( last_volume_, source.last_volume_); std::swap( prior_settlement_, source.prior_settlement_); } }; } // namespace

Matt Hurd wrote:
Yes, that is the classic solution but it never strikes me as quite what I need.
I've been struggling to find a cute solution I'm happy with. Mainly because I also typically want to expose the foo1_impl() for the cases when I wish to lock externally for multiple operations.
Andrei Alexandrescu's suggestion is to make foo1_impl take a lock: void foo1() { scoped_lock<> lock(mx_); foo1( lock ); } void foo1( scoped_lock<> & l ) { assert( l.locked() ); assert( l.mutex() == &mx_ ); // or not, to allow ext. sync // do things, incl. foo2( l ); } This doesn't solve the boilerplate problem, though. Maybe something like void foo1( scoped_lock<> * pl = 0 ) { assert( pl == 0 || pl->locked() ); scoped_lock<> internal_lock( mx_, pl == 0 ); if( pl == 0 ) pl = &internal_lock; // do things, incl. foo2( pl ); } where the boilerplate can be hidden in a class.

On Thu, 15 Jul 2004 16:03:36 +0300, Peter Dimov <pdimov@mmltd.net> wrote:
Matt Hurd wrote:
Yes, that is the classic solution but it never strikes me as quite what I need.
I've been struggling to find a cute solution I'm happy with. Mainly because I also typically want to expose the foo1_impl() for the cases when I wish to lock externally for multiple operations.
Andrei Alexandrescu's suggestion is to make foo1_impl take a lock:
void foo1() { scoped_lock<> lock(mx_); foo1( lock ); }
void foo1( scoped_lock<> & l ) { assert( l.locked() ); assert( l.mutex() == &mx_ ); // or not, to allow ext. sync
// do things, incl. foo2( l ); }
This doesn't solve the boilerplate problem, though. Maybe something like
void foo1( scoped_lock<> * pl = 0 ) { assert( pl == 0 || pl->locked() ); scoped_lock<> internal_lock( mx_, pl == 0 ); if( pl == 0 ) pl = &internal_lock;
// do things, incl. foo2( pl ); }
where the boilerplate can be hidden in a class.
Been thinking a little about it. I see some pros and cons to this approach. Pro: discrimination at run-time to enable different states to support different locking strategies. Pro: doesn't mess up methods that require bool template parameterisation Con: run-time overhead (or needing better compilers for global optimization than we currently have) Con: defaults on parameters messes up the interface if you want to have other parameters that are optional too. Con: requires a lock abstraction when you might be able to get away without one for your use case in the state your are dealing with. That is, don't need a lock, don't require one. The case for not requiring a lock should have zero cost at runtime. Stroustrup's don't pay for what you don't use principle. The caveat is that with modern compile time programming this should probably translate to something like, "with most modern compilers' optimization capabilities... don't pay...". Maybe a hybrid / dual approach is better. I am still trying to see this in a holistic sense were blocking/polling and non-threaded/threaded can be parameterized as well as allowing dual use of the objects. Would be nice to consider this interface w.r.t. other resource acquisition, such as file and socket i/o as the blocking, try, timed argument is still relevant to these resources as well. Along these lines perhaps locking can be considered one of many potential run-time preconditions or predicates that you may want to pay for a single evaluation/use sometimes to minimize the cost of state transitions. Locking is just one very common example. On the flip side, perhaps we should forget about it or give up ;-) , just layer a simple api on top of the common posix, win32, dce abstractions, like ace does and worry about C++ niceness later. I'd rather get it nice though as there are other libraries that do this already. Regards, Matt Hurd.

Howard Hinnant wrote:
I can't speak for the boost implementation (haven't carefully studied it), but the Metrowerks implementation also supports conditions operating on a recursive mutex. Our implementation (and I strongly suspect boost's as well) also supports conditions operating on non-recursive mutexes. The condition::wait<NonRecursiveMutex> /is/ a thin wrapper around pthread_cond_wait (when implemented on pthreads). Overhead is only incurred for condtion::wait<RecursiveMutex>.
On the other hand, our Windows implementation of condition::wait follows the algorithm laid out by Alexander Terekhov's "Algorithm 8a" posted in comp.programming.threads on April 27, 2001 (and noted as such in that file Alexander). In this case the wait() function does not differ depending upon the recursiveness of the mutex.
To clarify: does your implementation support waiting on a recursive mutex that has been locked more than once, temporarily decreasing its lock count to zero to allow other threads to acquire the mutex while the original thread is blocked by the wait?

On Jul 12, 2004, at 1:35 PM, Peter Dimov wrote:
On the other hand, our Windows implementation of condition::wait follows the algorithm laid out by Alexander Terekhov's "Algorithm 8a" posted in comp.programming.threads on April 27, 2001 (and noted as such in that file Alexander). In this case the wait() function does not differ depending upon the recursiveness of the mutex.
To clarify: does your implementation support waiting on a recursive mutex that has been locked more than once, temporarily decreasing its lock count to zero to allow other threads to acquire the mutex while the original thread is blocked by the wait?
That was the intent. But I stand corrected. I evidently got it wrong except on Mac OS X where I do explicitly decrement the count to 0 of the pthread_mutex during a wait (recursive mutex case only). Looks like I need to dig back into this on other platforms... -Howard

Howard Hinnant wrote:
On Jul 12, 2004, at 1:35 PM, Peter Dimov wrote:
On the other hand, our Windows implementation of condition::wait follows the algorithm laid out by Alexander Terekhov's "Algorithm 8a" posted in comp.programming.threads on April 27, 2001 (and noted as such in that file Alexander). In this case the wait() function does not differ depending upon the recursiveness of the mutex.
To clarify: does your implementation support waiting on a recursive mutex that has been locked more than once, temporarily decreasing its lock count to zero to allow other threads to acquire the mutex while the original thread is blocked by the wait?
That was the intent. But I stand corrected. I evidently got it wrong except on Mac OS X where I do explicitly decrement the count to 0 of the pthread_mutex during a wait (recursive mutex case only). Looks like I need to dig back into this on other platforms...
Doesn't this impose some overhead on your recursive_mutex, even if the user never takes advantage of this feature? (I have to admit that I don't have the slightest idea how is this possible to implement correctly.)

On Jul 12, 2004, at 4:51 PM, Peter Dimov wrote:
That was the intent. But I stand corrected. I evidently got it wrong except on Mac OS X where I do explicitly decrement the count to 0 of the pthread_mutex during a wait (recursive mutex case only). Looks like I need to dig back into this on other platforms...
Doesn't this impose some overhead on your recursive_mutex, even if the user never takes advantage of this feature? (I have to admit that I don't have the slightest idea how is this possible to implement correctly.)
I'm not familiar with how a native pthread_mutex is made recursive. But with a native non-recursive mutex, the added space overhead simply to handle recursive locking was also sufficient to negotiate use with condition variables without further space overhead needed just for the condition variables. To support the condition variables, a little more code is needed (maybe a dozen lines of C++) executed from within the wait function, and maybe a dozen or so bytes of stack space within the condition's wait function. Essentially the wait function saves the state of the mutex before the wait, then frees it for the wait, then restores the state of the mutex after the wait. With a non-recursive mutex you do the same thing, except that the state of the mutex is so much simpler: it is locked before and after the wait, and unlocked during. So there is no need to store mutex "state" on the stack for a non-recursive mutex. -Howard

Howard Hinnant wrote:
On Jul 12, 2004, at 4:51 PM, Peter Dimov wrote:
That was the intent. But I stand corrected. I evidently got it wrong except on Mac OS X where I do explicitly decrement the count to 0 of the pthread_mutex during a wait (recursive mutex case only). Looks like I need to dig back into this on other platforms...
Doesn't this impose some overhead on your recursive_mutex, even if the user never takes advantage of this feature? (I have to admit that I don't have the slightest idea how is this possible to implement correctly.)
I'm not familiar with how a native pthread_mutex is made recursive.
This kind of answers my question. ;-) See pthread_mutexattr_settype and PTHREAD_MUTEX_RECURSIVE. Note also that an implementation is allowed to make the default pthread_mutex recursive; in this case your users pay for the recursive overhead twice. Not that they don't deserve it for using a recursive_mutex. ;-)
But with a native non-recursive mutex, the added space overhead simply to handle recursive locking was also sufficient to negotiate use with condition variables without further space overhead needed just for the condition variables. To support the condition variables, a little more code is needed (maybe a dozen lines of C++) executed from within the wait function, and maybe a dozen or so bytes of stack space within the condition's wait function. Essentially the wait function saves the state of the mutex before the wait, then frees it for the wait, then restores the state of the mutex after the wait.
That's how Boost.Threads behaves, but (AFAICS) it doesn't protect itself against a thread switch and lock immediately after freeing the mutex for the wait, so it doesn't meet the "correctly" requirement. ;-)

Peter Dimov wrote: [...]
That's how Boost.Threads behaves, but (AFAICS) it doesn't protect itself against a thread switch and lock immediately after freeing the mutex for the wait, so it doesn't meet the "correctly" requirement. ;-)
I think it saves the lock count on the stack prior to releasing internal "real" [non-recursive, in a way, because it's never used recursively] mutex and restores the count on exit from cv.wait after acquiring the "real" mutex. In a way, the scheme is really similar to: http://groups.google.com/groups?selm=3BA5B950.6E21D198%40web.de http://groups.google.com/groups?selm=408E48E8.D67F1EA5%40web.de --- Per-thread flags can be used to turn nonrecursive locks into recursive ones. given: key - tsd flag key lock - mutex (non-recursive lock) count - unsigned init { tsd::init(&key); lock::init(&lock); count = 0; } destroy { lock::destroy(&lock); tsd::destroy(&key); } lock { if (!tsd::get(&key)) { tsd::set(&key, true); lock.acquire(); // assume throw()-nothing count = 1; } else { ++count; } } unlock { if (!--count ) { lock.release(); // throw()-nothing tsd::set(&key, false); } } Note that if you need "posix safety" with respect to unlock/destroy of this lock, "tsd::set(&key, false);" shall be done before "lock.release();" (and of course the underlying nonrecursive lock itself shall be safe with respect to unlock/destroy). --- where you just need to save-on-stack-and-reset/restore the lock count before-cv.enter_wait()/after-cv.exit_wait(). The problem I meant before was something else (don't recall now). regards, alexander.

On Jul 13, 2004, at 7:26 AM, Peter Dimov wrote:
Howard Hinnant wrote:
I'm not familiar with how a native pthread_mutex is made recursive.
This kind of answers my question. ;-)
See pthread_mutexattr_settype and PTHREAD_MUTEX_RECURSIVE. Note also that an implementation is allowed to make the default pthread_mutex recursive; in this case your users pay for the recursive overhead twice. Not that they don't deserve it for using a recursive_mutex. ;-)
I really hate English sometimes. I think I need an English compiler to tell me when I've written something ambiguous. :-\ What I meant was that when PTHREAD_MUTEX_RECURSIVE is defined, I'm not familiar with what the implementors have done with the internals of pthread_mutex, in comparison to when PTHREAD_MUTEX_RECURSIVE is not defined. I.e. what is the cost for the implementation to define PTHREAD_MUTEX_RECURSIVE? I can only speculate that that cost is probably similar to what I have to do to create a recursive mutex from a non-recursive one. The question I was answering concerned whether overhead was imposed for the use of a recursive mutex with a condition. I can only answer that question for the case where I've built the recursive mutex myself out of a non-recursive mutex. I've never implemented a native mutex library and so am not familiar with the costs at that level. If the OS provides a mutex that is recursive without documenting that fact, then that sounds like a docs bug to me. If I need a recursive mutex, I will gladly use an OS-supplied one if I can find such a beast. Otherwise I have to build it myself out of an OS-supplied non-recursive mutex. If I find that I've ended up paying for recursion overhead twice, or unnecessarily re-implemented recursive mutexes for that platform, I'll send a bug report to the OS vendor.
But with a native non-recursive mutex, the added space overhead simply to handle recursive locking was also sufficient to negotiate use with condition variables without further space overhead needed just for the condition variables. To support the condition variables, a little more code is needed (maybe a dozen lines of C++) executed from within the wait function, and maybe a dozen or so bytes of stack space within the condition's wait function. Essentially the wait function saves the state of the mutex before the wait, then frees it for the wait, then restores the state of the mutex after the wait.
That's how Boost.Threads behaves, but (AFAICS) it doesn't protect itself against a thread switch and lock immediately after freeing the mutex for the wait, so it doesn't meet the "correctly" requirement. ;-)
I've just reviewed my code and I am not seeing a possibility of this happening. After the state is saved, and the state is "freed", the code still owns a pthread_mutex_t protecting other threads from accessing (read or write) this modified state. Permission to do so is then granted using a pthread_cond_wait call with the pthread_mutex_t protecting the recursive mutex state. Upon return from the wait, the code again owns the pthread_mutex_t, and can atomically restore state without fear of another thread interfering (after checking of course for a spurious wake up). <shrug> I haven't reviewed the boost code, so I can neither agree nor disagree with your assessment of that implementation. I can only assert that I believe it is possible to do correctly, and that I believe the Metrowerks implementation does so on at least one platform (and also does it wrong on at least one platform). -Howard

Howard Hinnant wrote: [...]
If the OS provides a mutex that is recursive without documenting that fact, then that sounds like a docs bug to me. If I need a recursive mutex, I will gladly use an OS-supplied one if I can find such a beast. Otherwise I have to build it myself out of an OS-supplied non-recursive mutex. If I find that I've ended up paying for recursion overhead twice, or unnecessarily re-implemented recursive mutexes for that platform, I'll send a bug report to the OS vendor.
However in this case a recursive mutex has been provided by the OS, you just decided to not use it (in order to implement the recursive condition::wait). It seems that you have accepted the responsibility for the tradeoff. For some reason, it also seems that it's time for me to shut up.

On Jul 13, 2004, at 11:37 AM, Peter Dimov wrote:
Howard Hinnant wrote:
[...]
If the OS provides a mutex that is recursive without documenting that fact, then that sounds like a docs bug to me. If I need a recursive mutex, I will gladly use an OS-supplied one if I can find such a beast. Otherwise I have to build it myself out of an OS-supplied non-recursive mutex. If I find that I've ended up paying for recursion overhead twice, or unnecessarily re-implemented recursive mutexes for that platform, I'll send a bug report to the OS vendor.
However in this case a recursive mutex has been provided by the OS, you just decided to not use it (in order to implement the recursive condition::wait). It seems that you have accepted the responsibility for the tradeoff. For some reason, it also seems that it's time for me to shut up.
Fwiw, imho, you're wrong on both counts. But I'll only address the first: ;-) Here's a little history of the Metrowerks::threads lib. For one, it is fairly new, just released on Mac OS X last year. At that time, I believe 10.2.x was current (can't remember for sure) and 10.2.x did define PTHREAD_MUTEX_RECURSIVE, and I took advantage of that in Metrowerks::recursive_mutex. I also realized at the time that there might be some platforms on which I needed to support where PTHREAD_MUTEX_RECURSIVE wouldn't be defined, and I punted on that (I did not implement a recursive mutex, I downshifted to single thread mode). This was a temporary solution, and I knew I would have to do a better job before I really needed to ship on such a platform. It turns out that I was wrong. Unknown to me was that 10.1.x did not support PTHREAD_MUTEX_RECURSIVE. As I begin to receive these bug reports, I implemented a recursive_mutex based on pthreads, and exposed that implementation only when PTHREAD_MUTEX_RECURSIVE isn't defined. We have not yet shipped this fix, and I'm not the right person to ask for scheduling (I have no idea when it will ship). It is my intention (whether I actually succeed or not is another matter) that I use native recursive mutexes to implement Metrowerks::recursive_mutex when they are provided. Otherwise I implement Metrowerks::recursive_mutex using native non-recursive mutexes. With regards to native recursive mutexes, and their interaction with condition::wait: This is a situation that has come to my attention only with this thread, and I am grateful for that. This is one of the reasons I participate in boost. It makes a better MSL (that's what we call our library). I can report that for those platforms where native recursive mutexes are not available, and thus I have emulated them, the interaction with condition::wait works. I hope that clarifies things. I feel I have not clearly communicated in this thread and hope this post helps to rectify that. -Howard

Peter Dimov wrote: [...]
The mutex::timed_lock situation is a bit more complicated. POSIX labels pthread_mutex_timedlock as part of the Timeouts option, and I don't know whether this is because many implementations deliberatley do not provide timed locks, or because this is a relatively new addition to pthreads. Does someone know?
< Forward Quoted > David Butenhof wrote:
Alexander Terekhov wrote:
Pls read the message attached below. Perhaps you can answer the POSIX related (and also "thinking severals years ahead") question (to the newsgroup/list or to me -- I'll forward it then). I mean
The mutex::timed_lock situation is a bit more complicated. POSIX labels pthread_mutex_timedlock as part of the Timeouts option, and I don't know whether this is because many implementations deliberatley do not provide timed locks, or because this is a relatively new addition to pthreads.
Mutex timeout was devised for the "More realtime extensions" amendment, 1003.1d-1999. It's an option that is not required by the base standard or any profile (e.g., SUS), and that obviously was not in UNIX 98. I expect it is not widely implemented, nor to be a very high priority. (It's not in Tru64 or HP-UX, for example.)
In general, timed mutex operations are of relatively little use to general purpose applications in the first place. It's usually pretty hard to recover any shared data that may be corrupted because of unknown "bad behavior" on the part of the thread that failed to release the mutex. Because it's locked, you can't even unlock it, or reinitialize it; so you need to be able to dynamically switch to another mutex to continue operation. (As well as worrying about what the nonresponsive thread might be doing to your data.) As a result, there's usually not much you can do other than dump core in many cases. There are, of course, exceptions; but that's part of why the implementation priority has been relatively low on most systems.
This really was a feature desired by and intended for the truly "hard realtime" embedded system people, specifically for flight control systems and such that have no choice but to do "whatever is necessary".
(By the way, I've had this reply sitting in a buffer for days now, and just rediscovered it, entirely by coincidence, under a bunch of other windows. I've been having a lot of meetings and "stuff" lately, and this isn't the first time I've completely lost track of some message. Sorry. ;-) )
regards, alexander.

Eric Niebler wrote:
Peter Dimov wrote:
Michael Glassford wrote:
I agree that movable locks should wait. However, what about something like this, which doesn't require movable locks (concept only--there are probably errors):
[...]
Looks like Comeau in strict mode would still not accept it; noncopyable objects cannot be copy-initialized. But let's get back to my original question:
See the code I posted with my movable lock. It is noncopyable but you can return it from a function and use copy-initialization so you can declare your locks in an "if" statement. It compiles with Comeau strict.
One might argue that a lock is naturally moveable... but I think that we should constrain ourselves to (fixing) the current noncopyable scoped_lock idiom.
Should we consider alternate designs only after the fixed scoped_lock has achieved wide acceptance? After it has been proposed for standardization? After it has been accepted? If you're talking about making breaking changes to the threading library (aren't you?), isn't the time to consider alternate designs now?
Well, I was thinking of getting a design working that didn't require the lock being movable, then making it movable later. But if being movable is necessary for the design, it makes sense to consider it now. Actually, considering that that the promotion/demotion of read-locks and write-locks is similar to moving (when you promote by constructing a write-lock from a read-lock, it "moves" the lock), maybe it makes sense to consider it now anyway. There have also been requests from time to time for transferable locks, so it's something I would definitely like to address eventually. There was even a discussion once about implementing them once (for example, see http://lists.boost.org/MailArchives/boost/msg29881.php). Would Boost.Move (in the Sandbox, I guess) help with this? Mike

Michael Glassford <glassfordm@hotmail.com> writes:
Would Boost.Move (in the Sandbox, I guess) help with this?
Yes and yes. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

If boost is considering an alternative design I would like to suggest that an approach is taken where the synchronisation is parameterizable (is that a word?) so one can write to an exclusive/shared interface (commonly referred to as read/write) and substitute in a recursive, standard (just exclusive) or no_synch policy and the code will still work. This allows you to write to an shared/exclusive (r/w) style and have you code automatically usable in all the other modes. Also being able to write code that works in blocking and non-blocking (try or timed) mode is useful. I current use both these approaches with my code by wrapping the current boost libs and the thread_dev rw_mutex. Though my approach needs work. W.r.t to shared to exclusive (r to w) promotion, this will always be non-blocking with non-shared policies as you have the exclusive lock. In fact it is unnecessary and may be elided for non-shared cases. Same goes for exclusive to shared (r to w). This allows you to write better generic designs. Further to this, I use an approach, where I have locking and non-locking methods for interface access with mutex exposure. This allows efficient grouped operations, safe automatic use, and dual use objects where sometimes they are in a state that needs locking and sometimes not. My design here is unsatisfactory and I can see better ways of doing this. For the mythical boost policy pointer, I would also like to see this approach. The current shared_ptr performance gives me grief. As has been pointed out in another thread by others, because of the mutex (or double atomic ops if it was safe to change to this) it is half the speed of a normal single interlocked increment based pointer. Also, for many states in multi threaded libraries you don't care about the thread safety and thus the enormous cost (yes enormous is not understating it) of an atomic op or critical section is unfortunate. Regards, Matt Hurd.

Peter Dimov wrote:
Michael Glassford wrote:
I agree that movable locks should wait. However, what about something like this, which doesn't require movable locks (concept only--there are probably errors):
[...]
Looks like Comeau in strict mode would still not accept it; noncopyable objects cannot be copy-initialized. But let's get back to my original question:
This is certainly possible, but I don't see what the additional complexity buys us.
Well, in regards to simply choosing the right set of constructors, we appear to disagree on what's beneficial, so I guess I can't answer that. In regards to the alternative design Eric presented, or my responses to it, it would buy this syntax: if (try_lock l = ...) { } Not major, perhaps, but nice. Mike

"Peter Dimov" <pdimov@mmltd.net> writes:
Eric Niebler wrote:
How about:
if( ScopedLock l = try_lock( m ) ) { }
You know that I very much favor declarations in conditions, but this is a whole new design, one that uses moveable locks. Something like:
class Mutex { public:
AutoLock lock(); AutoLock try_lock(); AutoLock timed_lock( xtime ); };
One might argue that a lock is naturally moveable... but I think that we should constrain ourselves to (fixing) the current noncopyable scoped_lock idiom.
Are there really any repercussions from making a noncopyable type into a moveable type, other than that you might want to find ways to take advantage of the new moveability? -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

On Jul 8, 2004, at 11:10 AM, David Abrahams wrote:
Are there really any repercussions from making a noncopyable type into a moveable type, other than that you might want to find ways to take advantage of the new moveability?
If I had an rvalue reference available today, I would design the interface of the read and write locks differently. For example, instead of: void f(rw_mutex& m) { rw_mutex::upgradable_read_lock r(m); if (...) { rw_mutex::write_lock w(r); //... w.transfer_to(r); } //Point A } I would propose: void f(rw_mutex& m) { rw_mutex::upgradable_read_lock r(m); if (...) { rw_mutex::write_lock w(move(r)); //... r = move(w); } //Point A } The original code would not compile: write_lock would not accept an lvalue upgradable_read_lock and transfer_to would not exist. This would allow, among other things, locks to be returned from functions (as long as they were auto-local within the function). It would also allow for containers of locks. So the major repercussion that I see is that we're designing an inferior interface today that is not forward compatible with what I would propose if move semantics were available. We could possibly fake the desired move syntax today, but the thought makes me tired. :-( Especially in the light that the proposed move syntax is by no means a sure deal. -Howard

Howard Hinnant wrote:
On Jul 8, 2004, at 11:10 AM, David Abrahams wrote:
Are there really any repercussions from making a noncopyable type into a moveable type, other than that you might want to find ways to take advantage of the new moveability?
If I had an rvalue reference available today, I would design the interface of the read and write locks differently.
[reordered]
So the major repercussion that I see is that we're designing an inferior interface today that is not forward compatible with what I would propose if move semantics were available.
This is a concern. However, I'm not sure that the noncopyable lock interface is necessarily inferior and cannot be made forward compatible.
For example, instead of:
void f(rw_mutex& m) { rw_mutex::upgradable_read_lock r(m); if (...) { rw_mutex::write_lock w(r); //... w.transfer_to(r); } //Point A }
I would propose:
void f(rw_mutex& m) { rw_mutex::upgradable_read_lock r(m); if (...) { rw_mutex::write_lock w(move(r)); //... r = move(w); } //Point A }
The original code would not compile: write_lock would not accept an lvalue upgradable_read_lock and transfer_to would not exist.
I see no harm in allowing the "legacy" interface to function unmodified after the transition to moveable locks. (Or is it movable locks? You should really decide on that -eable vs -able thing, non-native speakers are getting confused. Think of the children.) I know that Classes should never move with copy syntax [Hinnant] but in this case the constructor that "moves" an upgradable_read_lock (upgradeable_read_lock?) to a write_lock is explicit, and it isn't a copy constructor. Likewise, I see no harm in retaining x.transfer_to( y ); as an alias to y = move(x).

Howard Hinnant <hinnant@twcny.rr.com> writes:
On Jul 8, 2004, at 11:10 AM, David Abrahams wrote:
Are there really any repercussions from making a noncopyable type into a moveable type, other than that you might want to find ways to take advantage of the new moveability?
If I had an rvalue reference available today, I would design the interface of the read and write locks differently. For example, instead of:
void f(rw_mutex& m) { rw_mutex::upgradable_read_lock r(m); if (...) { rw_mutex::write_lock w(r); //... w.transfer_to(r); } //Point A }
I would propose:
void f(rw_mutex& m) { rw_mutex::upgradable_read_lock r(m); if (...) { rw_mutex::write_lock w(move(r)); //... r = move(w); } //Point A }
The original code would not compile: write_lock would not accept an lvalue upgradable_read_lock and transfer_to would not exist.
I believe you can have the latter syntax with or without moveability. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

Peter Dimov wrote:
Michael Glassford wrote:
What about the following as an alternative? I retain the hierarchy (a TryLock is a Lock, a TimedLock is a TryLock) and resolve the conflict
(of
consistency within a concept and consistency across concepts) by the principle that a constructor always blocks unless you specifically tell it
not to:
Definitions ----------- M: appropriate mutex type
namespace lock_state { typedef enum { unlocked=0, locked=1 } lock_state; } //namespace lock_state
namespace read_write_lock_state { typedef enum { unlocked=0, read_locked=1, write_locked=2 } read_write_lock_state; } //namespace read_write_lock_state
namespace blocking_mode { typedef enum { non_blocking=0, blocking=1 } blocking_mode; } //namespace blocking_mode
ScopedLock / ScopedReadLock / ScopedWriteLock --------------------------------------------- lock(M) //always locking, blocking lock(M, lock_state) //blocking
ScopedTryLock / ScopedTryReadLock / ScopedTryWriteLock ------------------------------------------------------ try_lock(M) //always locking, BLOCKING try_lock(M, lock_state) //BLOCKING
try_lock(M, lock_state, blocking_mode) try_lock(M, blocking_mode) //always locking
ScopedTimedLock / ScopedTimedReadLock / ScopedTimedWriteLock ------------------------------------------------------------ timed_lock(M) //always locking, blocking timed_lock(M, lock_state) //blocking
timed_lock(M, lock_state, blocking_mode) timed_lock(M, blocking_mode) //always locking
timed_lock(M, t) //always locking, blocking for time t
This is certainly possible, but I don't see what the additional complexity buys us.
TryLock l( m, false );
if( l.try_lock() ) { }
looks acceptable to me.
TryLock l( m, non_blocking );
if( l.locked() ) { }
doesn't seem much of an improvement.
The two aren't equivalent (I'm not sure if you meant them to be): in your first example the constructor doesn't attempt to lock, and your second example it attempts a non-blocking lock. In any case, 1) What it buys over the current design: * Consistent behavior across lock types (which isn't true now: "lock_type l(M)" currently is blocking for Lock, non-blocking for TryLock, and doesn't exist for TimedLock AFAICS). * Consistent behavior within a lock type (which isn't true now: "try_lock l(M)" is non-blocking, while "try_lock l(M, true)" is blocking, which is rather arbitrary). The consistent behavior I mean is that constructors not taking a blocking_mode parameter always block. * The ability to specify all applicable lock types (blocking, non-blocking, timed-blocking) in a constructor. 2) What it buys over your suggestion (at the cost of complexity): * The ability to specify all applicable lock types (blocking, non-blocking, timed-blocking) in a constructor. 3) I'm proposing the use of enums rather than bools to indicate the initial state of the lock for these reasons: * Enums are more explicit, which is good. * Consistency with read/write locks (if we allow such critters): they can't use a bool because they have three states. * Having said that, now that I've changed my mind about what try_lock(M, true) should do (I now think it should block), my primary original motivation for using enums is no longer true: it was to prevent the meaning of try_lock(M, true) from changing silently. So I'm not opposed to continuing to use a bool if people feel strongly about it, or to providing both an enum and a bool version (possibly only temporarily). 4) Which leads to a problem with both your proposal and mine: they both silently break existing code (by changing try_lock(M) from non-blocking to blocking); at least, I presume you meant that. Would eliminating the lock_type(M) constructor (perhaps only for a release or two) be a good idea, or is a prominent warning in the documentation enough? Mike

Michael Glassford wrote:
Peter Dimov wrote:
[...]
This is certainly possible, but I don't see what the additional complexity buys us.
TryLock l( m, false );
if( l.try_lock() ) { }
looks acceptable to me.
TryLock l( m, non_blocking );
if( l.locked() ) { }
doesn't seem much of an improvement.
The two aren't equivalent (I'm not sure if you meant them to be): in your first example the constructor doesn't attempt to lock, and your second example it attempts a non-blocking lock.
I'm not sure where you think the two examples differ. Both attempt a non-blocking lock.
2) What it buys over your suggestion (at the cost of complexity):
* The ability to specify all applicable lock types (blocking, non-blocking, timed-blocking) in a constructor.
Why is that an improvement?
3) I'm proposing the use of enums rather than bools to indicate the initial state of the lock for these reasons: [...]
OK, let's leave the enum vs bool distinction aside for a while. The crucial difference is explicit try_lock() vs implicit try_lock (a constructor with the appropriate combination of arguments).
4) Which leads to a problem with both your proposal and mine: they both silently break existing code (by changing try_lock(M) from non-blocking to blocking); at least, I presume you meant that. Would eliminating the lock_type(M) constructor (perhaps only for a release or two) be a good idea, or is a prominent warning in the documentation enough?
My proposal (I'm not sure whether I should be credited for it; Howard posted locks that had these properties and I merely commented) can be made to break such code at compile time, simply by removing the nested TryMutex::scoped_try_lock typedef (as there is no longer a need for it, because ::scoped_lock is a TryLock or TimedLock, as appropriate).

On Jul 7, 2004, at 1:45 PM, Peter Dimov wrote:
My proposal (I'm not sure whether I should be credited for it; Howard posted locks that had these properties and I merely commented) can be made to break such code at compile time, simply by removing the nested TryMutex::scoped_try_lock typedef (as there is no longer a need for it, because ::scoped_lock is a TryLock or TimedLock, as appropriate).
Interesting... I hadn't thought about doing that. But I think I see where you're headed. A lock is just a lock, those parts of it you need, you instantiate and use. Those parts you don't, you ignore and they don't hurt anything (unless you try to explicitly instantiate your lock). If you try to use something the templated mutex doesn't support, you get a compile time error. Giving that some more thought... My intention with the read/write lock stuff was simply that these guys needed to at least support try locks. That is needed so that you can do assign-like locking between two objects: { lock_both<write_lock, read_lock> l(a.mut(), b.mut()); a = b; } Without try locks, lock_both can't be made to work without risking deadlock. -Howard

Howard Hinnant wrote:
On Jul 7, 2004, at 1:45 PM, Peter Dimov wrote:
My proposal (I'm not sure whether I should be credited for it; Howard posted locks that had these properties and I merely commented) can be made to break such code at compile time, simply by removing the nested TryMutex::scoped_try_lock typedef (as there is no longer a need for it, because ::scoped_lock is a TryLock or TimedLock, as appropriate).
Interesting... I hadn't thought about doing that. But I think I see where you're headed. A lock is just a lock, those parts of it you need, you instantiate and use. Those parts you don't, you ignore and they don't hurt anything (unless you try to explicitly instantiate your lock). If you try to use something the templated mutex doesn't support, you get a compile time error. Giving that some more thought...
Yes, it's even possible to collapse all locks into one templated class. My point was weaker than that. I wanted to collapse all three locks exposed by a TimedMutex into one, since with the linear concept hierarchy, there is no longer any reason to expose separate locks. A single TimedLock - called TimedMutex::scoped_lock - will suffice. Generic code that only needs a Mutex or TryMutex can still operate on a TimedMutex.

Peter Dimov wrote:
Michael Glassford wrote:
Peter Dimov wrote:
[...]
This is certainly possible, but I don't see what the additional complexity buys us.
TryLock l( m, false );
if( l.try_lock() ) { }
looks acceptable to me.
TryLock l( m, non_blocking );
if( l.locked() ) { }
doesn't seem much of an improvement.
The two aren't equivalent (I'm not sure if you meant them to be): in your first example the constructor doesn't attempt to lock, and your second example it attempts a non-blocking lock.
I'm not sure where you think the two examples differ. Both attempt a non-blocking lock.
It doesn't really matter, but if you're talking about the way Boost.Threads works currently (in the 1.31.0 release), according to both the code and the documentation, TryLock(m, false) makes no attempt to lock. If you're talking about a new design, then of course it means whatever you want :) That's one reason I prefer the enum: it's clear what the parameter actually means.
2) What it buys over your suggestion (at the cost of complexity):
* The ability to specify all applicable lock types (blocking, non-blocking, timed-blocking) in a constructor.
Why is that an improvement?
It seems reasonable that, if you're going to allow constructors to lock, you should allow them to lock in whatever ways are possible. I guess I don't feel that strongly about the issue, though; if a single constructor is strongly preferred by others, I guess that's OK with me. However, the constructors I suggested, especially as simplified by Howard, don't seem all that complicated to me. Also, see my comment below.
3) I'm proposing the use of enums rather than bools to indicate the initial state of the lock for these reasons: [...]
OK, let's leave the enum vs bool distinction aside for a while. The crucial difference is explicit try_lock() vs implicit try_lock (a constructor with the appropriate combination of arguments).
OK. I do generally like the idea of simplifying, and a single constructor is certainly simpler. The constructor locking is kind of nice for this syntax, though: if (try_lock l(...)) //or timed_lock { //do something }
4) Which leads to a problem with both your proposal and mine: they both silently break existing code (by changing try_lock(M) from non-blocking to blocking); at least, I presume you meant that. Would eliminating the lock_type(M) constructor (perhaps only for a release or two) be a good idea, or is a prominent warning in the documentation enough?
My proposal (I'm not sure whether I should be credited for it; Howard posted locks that had these properties and I merely commented) can be made to break such code at compile time, simply by removing the nested TryMutex::scoped_try_lock typedef (as there is no longer a need for it, because ::scoped_lock is a TryLock or TimedLock, as appropriate).
OK. I have to admit I didn't notice this part of the design. I'll have to go back and look again. Mike

Michael Glassford wrote:
Peter Dimov wrote:
[...]
This is certainly possible, but I don't see what the additional complexity buys us.
TryLock l( m, false );
if( l.try_lock() ) { }
looks acceptable to me.
TryLock l( m, non_blocking );
if( l.locked() ) { }
doesn't seem much of an improvement.
The two aren't equivalent (I'm not sure if you meant them to be): in your first example the constructor doesn't attempt to lock, and your second example it attempts a non-blocking lock.
I'm not sure where you think the two examples differ. Both attempt a non-blocking lock.
It doesn't really matter, but if you're talking about the way Boost.Threads works currently (in the 1.31.0 release), according to both the code and the documentation, TryLock(m, false) makes no attempt to lock. If you're talking about a new design, then of course it means whatever you want :)
I still don't see your point. Of course the constructor does not lock, but the try_lock() call in the if statement does.
That's one reason I prefer the enum: it's clear what the parameter actually means.
In this particular context, I don't think that the bool argument can be misinterpreted, and neither can the explicit 'l.try_lock()' call.
2) What it buys over your suggestion (at the cost of complexity):
* The ability to specify all applicable lock types (blocking, non-blocking, timed-blocking) in a constructor.
Why is that an improvement?
It seems reasonable that, if you're going to allow constructors to lock, you should allow them to lock in whatever ways are possible.
No, general principles say that it is usually unreasonable for a constructor to leave the object in a Heisenberg state. ;-) As a general rule, constructors establish invariants and therefore have postconditions (and throw if they fail to meet those postconditions). Operations that are expected to 'fail' are better expressed as (member) functions that return whether the operation succeeded.
The constructor locking is kind of nice for this syntax, though:
if (try_lock l(...)) //or timed_lock { //do something }
It is. Unfortunately the syntax is illegal. ;-)

Peter Dimov wrote:
Michael Glassford wrote:
Peter Dimov wrote:
[...]
This is certainly possible, but I don't see what the additional complexity buys us.
TryLock l( m, false );
if( l.try_lock() ) { }
looks acceptable to me.
TryLock l( m, non_blocking );
if( l.locked() ) { }
doesn't seem much of an improvement.
The two aren't equivalent (I'm not sure if you meant them to be): in your first example the constructor doesn't attempt to lock, and your second example it attempts a non-blocking lock.
I'm not sure where you think the two examples differ. Both attempt a non-blocking lock.
It doesn't really matter, but if you're talking about the way Boost.Threads works currently (in the 1.31.0 release), according to both the code and the documentation, TryLock(m, false) makes no attempt to lock. If you're talking about a new design, then of course it means whatever you want :)
I still don't see your point. Of course the constructor does not lock, but the try_lock() call in the if statement does.
Brain damage on my part, I guess. I've been reading and replying to many too many of these messages. All I meant is that the constructors themselves aren't the same, but you already knew that.
That's one reason I prefer the enum: it's clear what the parameter actually means.
In this particular context, I don't think that the bool argument can be misinterpreted, and neither can the explicit 'l.try_lock()' call.
2) What it buys over your suggestion (at the cost of complexity):
* The ability to specify all applicable lock types (blocking, non-blocking, timed-blocking) in a constructor.
Why is that an improvement?
It seems reasonable that, if you're going to allow constructors to lock, you should allow them to lock in whatever ways are possible.
No, general principles say that it is usually unreasonable for a constructor to leave the object in a Heisenberg state. ;-) As a general rule, constructors establish invariants and therefore have postconditions (and throw if they fail to meet those postconditions). Operations that are expected to 'fail' are better expressed as (member) functions that return whether the operation succeeded.
The constructor locking is kind of nice for this syntax, though:
if (try_lock l(...)) //or timed_lock { //do something }
It is. Unfortunately the syntax is illegal. ;-)
Yes, as I commented elsewhere it was too late at night and I missed that. See another message where I suggest a syntax that should work. Mike

I don't have strong feelings on the bool vs enum debate. But I do have some comments below... On Jul 7, 2004, at 10:31 AM, Michael Glassford wrote:
Definitions ----------- M: appropriate mutex type
namespace lock_state { typedef enum { unlocked=0, locked=1 } lock_state; } //namespace lock_state
ok.
namespace read_write_lock_state { typedef enum { unlocked=0, read_locked=1, write_locked=2 } read_write_lock_state; } //namespace read_write_lock_state
I'm not clear how these would be used. A read_lock is either locked or not, same with write_lock. I'm not seeing a need, nor a desire for a lock that can both read lock and write lock.
namespace blocking_mode { typedef enum { non_blocking=0, blocking=1 } blocking_mode; } //namespace blocking_mode
ok.
ScopedLock / ScopedReadLock / ScopedWriteLock --------------------------------------------- lock(M) //always locking, blocking lock(M, lock_state) //blocking
ok.
ScopedTryLock / ScopedTryReadLock / ScopedTryWriteLock ------------------------------------------------------ try_lock(M) //always locking, BLOCKING try_lock(M, lock_state) //BLOCKING
ok.
try_lock(M, lock_state, blocking_mode) try_lock(M, blocking_mode) //always locking
try_lock tl(M, unlocked, blocking); What does this mean? The same as: try_lock tl(M, unlocked); ? Similarly is not try_lock(M, locked, blocking_mode) the same as try_lock(M, blocking_mode)? If I'm interpreting this correctly, how about the following reduced list to eliminate redundancy: try_lock(M) //always locking, BLOCKING try_lock(M, lock_state) //BLOCKING (if attempting to lock) try_lock(M, blocking_mode) //always (attempt) locking Btw, this would require enums over bool.
ScopedTimedLock / ScopedTimedReadLock / ScopedTimedWriteLock ------------------------------------------------------------ timed_lock(M) //always locking, blocking timed_lock(M, lock_state) //blocking
timed_lock(M, lock_state, blocking_mode) timed_lock(M, blocking_mode) //always locking
timed_lock(M, t) //always locking, blocking for time t
Note that the addition of the try_lock stuff to ScopedTimedLock is a change. I don't disagree with that change. Just hilighting it. It is essentially alternative syntax for a timed_lock specified with a 0 delta time. Speaking of which, I've never been happy with "t" in the ScopedTimedLock docs. I've always felt it was ambiguous. Does t represent an absolute time, or a change in time? Both interpretations are useful. In the Metrowerks::threads lib I have: scoped_timed_lock(mutex_type& m, const universal_time& unv_time); scoped_timed_lock(mutex_type& m, const elapsed_time& elps_time); Where universal_time and elapsed_time are simplistic structs (like the boost time), but represent absolute and delta times respectively. They can also be added and subtracted as makes sense (e.g. you can add a universal_time and an elapsed_time, or two elapsed_time's, but you can't add two universal_time's). I took the Posix route and defined universal_time as the number of seconds and nanoseconds since midnight Jan. 1, 1970, UTC. The universal_time default constructor returns the current time. So how about: timed_lock(M) //always locking, blocking timed_lock(M, lock_state) //blocking timed_lock(M, blocking_mode) //always locking timed_lock(M, universal_time u) //always locking, blocking until u timed_lock(M, elapsed_time t) //always locking, blocking for elapsed time t ? -Howard

Howard Hinnant wrote:
I don't have strong feelings on the bool vs enum debate. But I do have some comments below...
On Jul 7, 2004, at 10:31 AM, Michael Glassford wrote:
Definitions ----------- M: appropriate mutex type
namespace lock_state { typedef enum { unlocked=0, locked=1 } lock_state; } //namespace lock_state
ok.
namespace read_write_lock_state { typedef enum { unlocked=0, read_locked=1, write_locked=2 } read_write_lock_state; } //namespace read_write_lock_state
I'm not clear how these would be used. A read_lock is either locked or not, same with write_lock. I'm not seeing a need, nor a desire for a lock that can both read lock and write lock.
Earlier in this discussion I gave a lot of examples of cases where separate read_lock and write_lock classes had problems. I'll have to go back over my examples with more recent discussion in mind to see if the problems have been addressed before I can comment on this.
namespace blocking_mode { typedef enum { non_blocking=0, blocking=1 } blocking_mode; } //namespace blocking_mode
ok.
ScopedLock / ScopedReadLock / ScopedWriteLock --------------------------------------------- lock(M) //always locking, blocking lock(M, lock_state) //blocking
ok.
ScopedTryLock / ScopedTryReadLock / ScopedTryWriteLock ------------------------------------------------------ try_lock(M) //always locking, BLOCKING try_lock(M, lock_state) //BLOCKING
ok.
try_lock(M, lock_state, blocking_mode) try_lock(M, blocking_mode) //always locking
try_lock tl(M, unlocked, blocking);
What does this mean? The same as:
try_lock tl(M, unlocked); ?
Yes. A while back I was working on a way to eliminate such degenerate cases that using structs instead of enums to select an appropriate constructor at compile time, but it had the disadvantage of being unable to select the appropriate behavior at run time, so I shelved the idea.
Similarly is not try_lock(M, locked, blocking_mode) the same as try_lock(M, blocking_mode)? If I'm interpreting this correctly, how about the following reduced list to eliminate redundancy:
try_lock(M) //always locking, BLOCKING try_lock(M, lock_state) //BLOCKING (if attempting to lock)
try_lock(M, blocking_mode) //always (attempt) locking
I wouldn't object to that. I think the reason I hadn't made this simplification already is that I originally thought try_lock(M, lockstate) should not block, but now that I've changed my mind about that it makes sense.
Btw, this would require enums over bool.
ScopedTimedLock / ScopedTimedReadLock / ScopedTimedWriteLock ------------------------------------------------------------ timed_lock(M) //always locking, blocking timed_lock(M, lock_state) //blocking
timed_lock(M, lock_state, blocking_mode) timed_lock(M, blocking_mode) //always locking
timed_lock(M, t) //always locking, blocking for time t
Note that the addition of the try_lock stuff to ScopedTimedLock is a change. I don't disagree with that change. Just hilighting it. It is essentially alternative syntax for a timed_lock specified with a 0 delta time.
Yes, it's a change; thanks for highlighting it. I think that either TimedLock should refine TryLock, or else that Lock, TryLock, and TimedLock should be completely separate (as Vladimir suggested), none being refinements of any of the others.
Speaking of which, I've never been happy with "t" in the ScopedTimedLock docs. I've always felt it was ambiguous. Does t represent an absolute time, or a change in time? Both interpretations are useful. In the Metrowerks::threads lib I have:
scoped_timed_lock(mutex_type& m, const universal_time& unv_time); scoped_timed_lock(mutex_type& m, const elapsed_time& elps_time);
Where universal_time and elapsed_time are simplistic structs (like the boost time), but represent absolute and delta times respectively. They can also be added and subtracted as makes sense (e.g. you can add a universal_time and an elapsed_time, or two elapsed_time's, but you can't add two universal_time's). I took the Posix route and defined universal_time as the number of seconds and nanoseconds since midnight Jan. 1, 1970, UTC. The universal_time default constructor returns the current time.
So how about:
timed_lock(M) //always locking, blocking timed_lock(M, lock_state) //blocking
timed_lock(M, blocking_mode) //always locking
timed_lock(M, universal_time u) //always locking, blocking until u timed_lock(M, elapsed_time t) //always locking, blocking for elapsed time t
Yes, something needs to be done about time. There was a discussion of it recently by people who care about time issues, but I wasn't able to devote enough time to it then, and haven't gotten back to it. Perhaps we can address that issue later? The issues we're already dealing with (lock concept taxonomies, constructors, read/write locks, promotion) are complex enough for me already. Mike

Howard Hinnant wrote:
Speaking of which, I've never been happy with "t" in the ScopedTimedLock docs. I've always felt it was ambiguous. Does t represent an absolute time, or a change in time? Both interpretations are useful.
It represents an absolute time. The main rationale for using absolute times is condition::timed_wait, which needs to be called in a loop. A relative time would need to be recalculated for each iteration. But relative times are useful, too.
In the Metrowerks::threads lib I have:
scoped_timed_lock(mutex_type& m, const universal_time& unv_time); scoped_timed_lock(mutex_type& m, const elapsed_time& elps_time);
Where universal_time and elapsed_time are simplistic structs (like the boost time), but represent absolute and delta times respectively. They can also be added and subtracted as makes sense (e.g. you can add a universal_time and an elapsed_time, or two elapsed_time's, but you can't add two universal_time's). I took the Posix route and defined universal_time as the number of seconds and nanoseconds since midnight Jan. 1, 1970, UTC. The universal_time default constructor returns the current time.
One problem with the UTC time is that it can jump backwards, if the system time changes. Maybe we should consider using (or adding) a monotonic time.

Michael Glassford <glassfordm@hotmail.com> writes:
The point being that TryLock is a refinement of Lock, and TimedLock is a refinement of TryLock? If so, I agree that's better than TryLock and TimedLock both refining Lock. There are still the constructor issues to deal with in such a lock taxonomy, however. The main one: how do you define destructors that are consistent within a lock concept and also consistent between concepts. E.g., on the one hand it seems that a one-parameter constructor should do the same thing in all lock types--so it would have to block; on the other hand, it seems that TryLock constructors should not block unless instructed to do otherwise.
One possible answer is that construction protocol simply isn't part of the concept taxonomy. Another possible answer might lie in considering syntax alternatives for lock construction. For example, a named parameter interface: Lock l(block = m); TryLock l(block = m); TryLock l(proceed = m); TimedLock l(wait = m, t); That particular expression of the idea is probably too cute, but I'm just trying to expand the boundaries of thought. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote:
Michael Glassford <glassfordm@hotmail.com> writes:
The point being that TryLock is a refinement of Lock, and TimedLock is a refinement of TryLock? If so, I agree that's better than TryLock and TimedLock both refining Lock. There are still the constructor issues to deal with in such a lock taxonomy, however. The main one: how do you define destructors that are consistent within a lock concept and also consistent between concepts. E.g., on the one hand it seems that a one-parameter constructor should do the same thing in all lock types--so it would have to block; on the other hand, it seems that TryLock constructors should not block unless instructed to do otherwise.
One possible answer is that construction protocol simply isn't part of the concept taxonomy.
Another possible answer might lie in considering syntax alternatives for lock construction. For example, a named parameter interface:
Lock l(block = m);
TryLock l(block = m); TryLock l(proceed = m);
TimedLock l(wait = m, t);
That particular expression of the idea is probably too cute,
It is an interesting idea, though.
but I'm just trying to expand the boundaries of thought.
I appreciate that; I've been trying to think of alternatives, but hadn't considered named parameters. Mike

On Jul 7, 2004, at 9:19 AM, Michael Glassford wrote:
There are still the constructor issues to deal with in such a lock taxonomy, however. The main one: how do you define destructors that are consistent within a lock concept and also consistent between concepts. E.g., on the one hand it seems that a one-parameter constructor should do the same thing in all lock types--so it would have to block; on the other hand, it seems that TryLock constructors should not block unless instructed to do otherwise.
Agreed, and I think the scoped_try_lock got it wrong (i.e. scoped_try_lock(mut) should block). This somewhat irritates me as I have coded and shipped Metrowerks::scoped_try_lock with the wrong semantics (doesn't block). :-\ I'm finding more and more uses for generic lock code. The generic helper I showed yesterday was one. Another is: template <class TryLock1, class TryLock2> class lock_both { public: lock_both(TryLock1& m1, TryLock2& m2, bool lock_it = true); ~lock_both(); void lock(); bool try_lock(); void unlock(); bool locked() const; operator int bool_type::* () const; private: lock_both(const lock_both&); lock_both& operator=(const lock_both&); }; which allows you to lock two locks without fear of deadlock. So consistent syntax/semantics across lock types is a real win. -Howard

Howard Hinnant wrote:
On Jul 7, 2004, at 9:19 AM, Michael Glassford wrote:
There are still the constructor issues to deal with in such a lock taxonomy, however. The main one: how do you define destructors that are consistent within a lock concept and also consistent between concepts. E.g., on the one hand it seems that a one-parameter constructor should do the same thing in all lock types--so it would have to block; on the other hand, it seems that TryLock constructors should not block unless instructed to do otherwise.
Agreed, and I think the scoped_try_lock got it wrong (i.e. scoped_try_lock(mut) should block).
I've come to agree with you on this, and hope to fix it one way or another (see other replies in this discussion).
This somewhat irritates me as I have coded and shipped Metrowerks::scoped_try_lock with the wrong semantics (doesn't block). :-\
I'm finding more and more uses for generic lock code.
I'm glad to know this; I wondered at first whether generic uses of locks would be at all common or important.
The generic helper I showed yesterday was one. Another is:
template <class TryLock1, class TryLock2> class lock_both { public:
lock_both(TryLock1& m1, TryLock2& m2, bool lock_it = true);
~lock_both();
void lock(); bool try_lock(); void unlock(); bool locked() const; operator int bool_type::* () const;
private: lock_both(const lock_both&); lock_both& operator=(const lock_both&); };
which allows you to lock two locks without fear of deadlock. So consistent syntax/semantics across lock types is a real win.
Agreed. I always like as much consistency as possible, which is why I started the whole lock constructor discussion. Mike

Howard Hinnant wrote: [big snip]
The downside to all this is that the sizeof(rw_mutex) about doubled to support upgradable_read_lock (whether or not you use that functionality).
Really? (Not having tried to implement it,) that surprises me. Can you give a few more details about why? I'm interested.
The code size of the read and write locking/unlocking also swelled somewhat. I'm still contemplating the cost/functionality tradeoff, but my current feeling is that it is probably worth it.
Mike

On Jul 8, 2004, at 12:25 AM, Michael Glassford wrote:
Howard Hinnant wrote:
[big snip]
The downside to all this is that the sizeof(rw_mutex) about doubled to support upgradable_read_lock (whether or not you use that functionality).
Really? (Not having tried to implement it,) that surprises me. Can you give a few more details about why? I'm interested.
I had implemented rw_mutex with some conditions and integral types. In adding upgradable_read support I doubled the number of both. Sorry to be vague, but I'm only prepared to open source the interface. Metrowerks is selling the implementation for profit (or may in the future anyway). I'm interested in open sourcing the interface because I would like to see this or a similar library standardized in C++0X. Dinkumware also sells a boost-derived threads lib. So between boost and two commercial vendors for field experience, I think a standardized interface is a reasonable possibility for C++0X. Just need to gather consensus and get that proposal written... :-) -Howard

=?iso-8859-15?Q?Bruno_Mart=EDnez_Aguerre?= <br1@internet.com.uy> writes:
What about a latent_write_lock?
It's semantics would be to block on contruction until it only shares the mutex with read_locks. A write_lock could be contructed from a latent_write_lock and this would always work, because another latent_write_lock would still be blocked in it's constructor. This kind of lock would be useful for situations in which you have to read a structure to know if you have to write on it.
Yes! The RTOS we use does this --- you can have an "intention for exclusive access", which happily shares with "shared access", but not with other "intention for exclusive access" or "exclusive access". Promotion is always safe (though blocking), since there can only be one promotable lock. It also means that data read during the read-only part of the code can safely be assumed to be valid during when you have acquired your write lock. void g(read_write_mutex& m) { read_lock r(m); // read stuff } void f(read_write_mutex& m) { latent_write_lock l(m); // other threads calling f will block here. // threads calling g will not block. // other threads can read here int const balance=get_balance(); int const cost=get_cost(); if(cost<balance) { write_lock w(l); // block whilst we wait for other readers to finish // exclusive access; even threads calling g will block assert(balance==get_balance()); // no re-reading necessary set_balance(balance-cost); } // other threads can read again } Anthony -- Anthony Williams Senior Software Engineer, Beran Instruments Ltd.

On Jul 5, 2004, at 8:27 PM, Matthew Vogt wrote:
I think that in the second case, an exception should be thrown. Typically, if you fail to promote a rw lock from read to write, then you will have to repeat the part of the code that was read-protected after the other thread's write lock has been released: <snip> // Try the whole thing again
Exploring the case: throw exception if unable to atomically promote read to write: If an exception is not thrown, then you are guaranteed what you previously read is still true, and all is well. If an exception is thrown, then you have to re-read to determine current state. Hmm... Given that when you have write privilege you also implicitly have read privilege, this seems a little extreme. Would it not be both more efficient and easier to follow if you just reaffirm your state under the write lock rather than assume your state, and write a catch block to reassert it if it happens to be false? That is, this: bool try_sale(rw_mutex& m) { read_lock rl(m); long balance = get_balance(); long cost = get_total_cost(); if (balance > cost) { write_lock wl(rl); set_balance(balance - cost); // ... return true; } return false } bool do_sale(rw_mutex& m) { while (true) { try_again: try { return try_sale(m); catch (rw_promotion_error&) { // Try the whole thing again // how is this implemented? goto try_again; // ? } } } vs this: void do_sale(rw_mutex& m) { read_lock rl(m); long balance = get_balance(); long cost = get_total_cost(); if (balance > cost) { rl.unlock(m); write_lock wl(m); set_balance(balance - cost); wl.transfer_to_read_lock(rl); balance = get_balance(); } // ... }
But this doesn't achieve anything - if you release the read lock, then you don't know that what held true while it was read-locked is still true once you have acquired a write lock.
If you choose to have lock promotion (having only lock demotion is a viable choice, except that there will be certain usage patterns that are more efficient with a fail-able promotion), then it must at least preserve known state across promotion.
My assertion is that you don't know a-priori if the read-to-write promotion achieves anything or not. If you allow it to fail, by exception, or by error code, or by any other means, then you have to write code to re-read the current state. If you don't allow it to fail, then you must make it block until it succeeds. The latter seems simpler in logic, and smaller in code size. The logic is quite simple: read_lock to write_lock promotion is not atomic. Once that is understood, the programmer can just deal with it. Trying to encapsulate this bit of reality only serves to obfuscate the necessary logic. -Howard

On Jul 5, 2004, at 10:35 PM, Howard Hinnant wrote:
vs this:
void do_sale(rw_mutex& m) { read_lock rl(m);
long balance = get_balance(); long cost = get_total_cost();
if (balance > cost) { rl.unlock(m); write_lock wl(m); set_balance(balance - cost); wl.transfer_to_read_lock(rl); balance = get_balance(); } // ... }
Oops, that should be: vs this: void do_sale(rw_mutex& m) { read_lock rl(m); long balance = get_balance(); long cost = get_total_cost(); if (balance > cost) { rl.unlock(m); write_lock wl(m); cost = get_total_cost(); set_balance(get_balance() - cost); wl.transfer_to_read_lock(rl); balance = get_balance(); } // ... } but still looks much simpler, and smaller code size to me. -Howard

Howard Hinnant wrote:
Oops, that should be:
vs this:
void do_sale(rw_mutex& m) { read_lock rl(m);
long balance = get_balance(); long cost = get_total_cost();
if (balance > cost) { rl.unlock(m); write_lock wl(m); cost = get_total_cost(); set_balance(get_balance() - cost); wl.transfer_to_read_lock(rl); balance = get_balance(); } // ... }
but still looks much simpler, and smaller code size to me.
The read lock is pointless in this case. The idea is that get_balance() is slow, so we want to perform it under only a read lock, to avoid stalling readers unnecessarily. However your code performs it again under the write lock anyway. It's much simpler to just do the whole operation under a write lock. These examples are mostly what led me to believe that promotion isn't very useful. But I may be missing an important use case. Demotion, on the other hand, seems useful: int deposit( int amount ) // requires: amount > 0 // returns: new balance (atomic) { write_lock wr( m ); v.push_back( amount ); read_lock rd( wr ); int r = 0; accumulate( v.begin(), v.end(), r ); return r; }

Peter Dimov <pdimov <at> mmltd.net> writes:
The read lock is pointless in this case. The idea is that get_balance() is slow, so we want to perform it under only a read lock, to avoid stalling readers unnecessarily. However your code performs it again under the write lock anyway. It's much simpler to just do the whole operation under a write lock.
These examples are mostly what led me to believe that promotion isn't very useful. But I may be missing an important use case.
Promotion is not necessary, but it can improve peformance - if you have a case where you must perform some work before knowing whether you need to perform an update, then the cost of performing that work with exclusive access may be prohibitive. This cost must be balanced against the potential cost of having to perform the same work a second time in the case the promotion fails. I would suppose that promotion is most suitable to a case where there are many readers, each of which has a low probability of having to do an update. I can't immediately think of an example that would present such a case, however, so it may be more theoretical than practical. Matt

Howard Hinnant <hinnant <at> twcny.rr.com> writes:
Exploring the case: throw exception if unable to atomically promote read to write:
If an exception is not thrown, then you are guaranteed what you previously read is still true, and all is well. If an exception is thrown, then you have to re-read to determine current state.
Hmm...
Given that when you have write privilege you also implicitly have read privilege, this seems a little extreme. Would it not be both more efficient and easier to follow if you just reaffirm your state under the write lock rather than assume your state, and write a catch block to reassert it if it happens to be false?
If you have to re-check the state under a promoted lock, then you don't need lock promotion - you just need to begin the operation a second time, with a write lock. This will be exactly the same thing, with the various pieces of code correctly scoped.
void do_sale(rw_mutex& m) { read_lock rl(m);
long balance = get_balance(); long cost = get_total_cost();
if (balance > cost) { rl.unlock(m); write_lock wl(m); set_balance(balance - cost);
You can't do this operation without first checking that the cost and balance have not changed since the read lock was unlocked.
wl.transfer_to_read_lock(rl); balance = get_balance(); } // ... }
[snip]
My assertion is that you don't know a-priori if the read-to-write promotion achieves anything or not. If you allow it to fail, by exception, or by error code, or by any other means, then you have to write code to re-read the current state.
If a promotion fails, you not only need to read state again, you also need to release your existing read lock to allow the thread that beat you to lock promotion to proceed. With scoped locks, I think it is easier to handle this with an exception, and a repeat of the failed portion of code, than to release and re-acquire the locks manually.
If you don't allow it to fail, then you must make it block until it succeeds.
This isn't possible, since if you fail promotion, you must also release your extant read lock to allow the other promoted lock to proceed. Actually, I guess it is possible, but the code still reads incorrectly with the naive assumption that a scoped lock is maintained throughout its scope.
The latter seems simpler in logic, and smaller in code size. The logic is quite simple:
read_lock to write_lock promotion is not atomic.
If it isn't atomic, then it isn't promotion - it is misleading to have it in the interface, since the programmer must be aware that promotion may have invalidated any prior knowledge that had been protected by the earlier read lock.
Once that is understood, the programmer can just deal with it. Trying to encapsulate this bit of reality only serves to obfuscate the necessary logic.
-Howard
Yes. But OTOH, it can be atomic-with-the-possibility-of-failure, which has the benefit of not requiring state to be re-checked. It also has clear semantics in the case of failure which the programmer can deal with. Matt

Howard Hinnant wrote: Read this thread: http://groups.google.com/groups?threadm=ijnd7a.3pb.ln%40tux.localdomain (Subject: upgradeable lock) I've quoted below a few bits.
read_lock to write_lock promotion is not atomic.
Butenhof: IF one allows only one "upgradeable reader", (UR) as a special type of access (as he suggested), you certainly could make the guarantee. You can allow the single UR concurrent access with any number of readers. When the UR requests upgrade, it releases its read access and in effect becomes the HEAD of the writer queue. Once all readers have released (there's still no chance that the data could have been modified from the state last observed by the UR), the waiting UR must be immediately granted write access and scheduled to run. Normally, one is better off making a RW lock behave like normal POSIX mutexes, where awakened waiters compete equally with running threads for access. (Rather than forced "FIFO" ownership that, like Ada rendezvous, causes completely wasted context switches.) In this case, though, we can't do that because the UR would lose atomicity. That's a BIG inefficiency. You'd really be better off breaking the atomicity guarantee and requiring the UR to re-evaluate the data after it gains write access. In which case you can allow multiple upgradeable readers. However it also means there's no longer any point to allowing upgrade... you might as well just release the read lock and contend normally for a write lock. Which is precisely why "upgrade" isn't in POSIX.
Once that is understood, the programmer can just deal with it. Trying to encapsulate this bit of reality only serves to obfuscate the necessary logic.
Me: [...] how about the following "extensions": a) ability for a reader to request a write-upgrade with an indication whether it ended up being "atomic" or not. b) upgrade operation could either result in a "write" ownership always or (optionally) in "read-again ;-)", if it ended up being non-atomic. c) ability for a writer to query whether some upgrades are pending -- that would allow some extra "optimizations" (basically he could then keep track of updated/invalidated stuff, to allow *conditional* re-calcs for non-atomic upgraders), I think. Butenhof: Yes, if you treat upgradeability as an "optimization" on which nobody can ever depend. Any reader can request upgrade, and one of the readers may be granted an atomic upgrade while the others need to re-evaluate. Very dangerous, in my opinion, because it'd be trivially easy to write bad code that'll be really hard to test. You're adding a weird beastie: an "alternate success" status that's not really an error. "You got it; but it's not what you may think." What would we call it? EALMOST? Or maybe EYOUASKEDFORITBUTYOUDIDNTGETIT? Actually, you'd be better off with a "request upgrade" that'd either convert or return failure leaving the caller with read access. This fits much better with the normal UNIX "do this or fail without changing anything" model. And, OK, one could deal with the naming issues. Documentation, though, would be easily misunderstood. (That MUST be a prime concern in the design of any interface.) And, in the end, what do you have? Every call site that requests upgrade MUST be able to handle either return, but there's no way to predict for sure whether any given call will succeed or "not quite succeed". Easy to misunderstand and misuse, tough to test. Bad combination. The query operation sounds like an invitation to even greater screwups, to me. More complication, harder to test... for very little added value. Me: Well, I guess that one could argue that the whole read/write locking concept is just an "'optimization' on which nobody can ever depend". ;-) Butenhof: Well, yes, given the looseness of definitions to allow experimentation in read/write preference, and all that, it's true that the actual guarantee for concurrent read access is slim at best. (In fact, it provides for an "implemention defined" limit on concurrent readership, and there's nothing to prevent an implementation from defining that to the value "1".) regards, alexander.

Howard Hinnant wrote:
bool try_sale(rw_mutex& m) { read_lock rl(m);
long balance = get_balance(); long cost = get_total_cost();
if (balance > cost) { write_lock wl(rl); set_balance(balance - cost); // ... return true; } return false }
bool do_sale(rw_mutex& m) { while (true) { try_again: try { return try_sale(m); catch (rw_promotion_error&) { // Try the whole thing again // how is this implemented? goto try_again; // ? } } }
vs this:
void do_sale(rw_mutex& m) { read_lock rl(m);
long balance = get_balance(); long cost = get_total_cost();
if (balance > cost) { rl.unlock(m); write_lock wl(m); set_balance(balance - cost); wl.transfer_to_read_lock(rl); balance = get_balance(); } // ... }
Both are broken, unfortunately. :-) The first one is broken, because the whole point of postponing the write lock is to allow readers to operate at full speed. However, if readers operate at full speed, then there is no guarantee that the write lock will ever succeed; ergo, starvation. So promotion is good only for non-critical updates. The second one is also broken but I see that you corrected it in a follow-up post.

Peter Dimov wrote: [...]
The first one is broken, because the whole point of postponing the write lock is to allow readers to operate at full speed. However, if readers operate at full speed, then there is no guarantee that the write lock will ever succeed; ergo, starvation. So promotion is good only for non-critical updates.
Not quite. It depends. I have a read/write lock which is "almost" starvation-free (it uses single "entry" lock for both readers and writes, so that the scheduling protocol for that entry lock drives the whole read/write locking without the need to choose between reader or writer preference policy which could cause reader or writer starvation). Upgradeability was the design driving force... http://groups.google.com/groups?selm=3B166244.F923B993%40web.de regards, alexander.

Howard Hinnant wrote:
So in:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m);
int y = x;
if (...) { read_write_mutex::write_lock w(r); //lock promotion
what would happen here if two threads entered f and tried to construct w simultaneously?
My take, depending upon how the w(r) constructor is implemented, is either:
1. deadlock. 2. one thread blocks and one thread gets the lock.
Imho choice 1 is a buggy implementation of the w(r) constructor. And choice 2 results in the assert(x==y) possibly firing.
I believe choice 2 is the only reasonable path.
Both choices are buggy. 3. One thread gets the lock, the other throws lock_error. is the only reasonable implementation of this operation, if it is provided.
And I also believe that the syntax above might lull a code reader into believing that assert(x==y) should always be true.
As it will be. There is no point in having the syntax otherwise, as the statement would have no reliable postconditions.
And therefore I think the following (more explicit) syntax is superior:
void f(read_write_mutex m) { read_write_mutex::read_lock r(m);
int y = x;
if (...) { r.unlock(); read_write_mutex::write_lock w(m);
This syntax is already available, and it does what it's supposed to do, but it's not the same as "promotion", which is atomic. I have to admit that I'm not convinced that unconditional promotion is useful, and I have trouble coming up with use cases for conditional promotion, too.

Michael Glassford wrote:
Doug Gregor wrote:
Refinements of a concept can only add restrictions, they cannot remove them.
True. But it's not hard to define the concepts in such a way that the lock classes can have different constructors:
Lock concept defines all lock operations except constructors. TryLock concept refines Lock by adding try_* methods. TimedLock concept refines TryLock by adding timed_* methods.
ScopedLock refines Lock by adding appropriate constructors. ScopedTryLock refines TryLock by adding appropriate constructors. ScopedTimedLock refines TimedLock by adding appropriate constructors.
Yes, it's possible to define them, but it'd be pointless to do so. The idea of a concept X that refines concept Y is that a generic function can use a X as if it were an Y. In this case, all refinements are wasted because construction is a very important property of the concept, since the typical usage pattern creates scoped locks locally, instead of receiving them already constructed.

Michael Glassford wrote:
I would consider this more an argument for eliminating 1-argument constructors entirely (except for scoped_lock, where its meaning is completely unambiguous):
It does appear that the design of the Scoped*Lock classes was very deliberate in having distinctly different constructor semantics. Though it's curious that all three have a L l(m,b) constructor so that { LockType l(m, true); } will always have blocking semantics, regardless of lock type, and that { LockType l(m, false); } will always create an unlocked lock. This being the case, it would be a convenient shorthand to add { LockType l(m); } to all three Scoped*Lock concepts to support the generic case of a blocking lock. But it's just a shorthand, so it can be relegated to a wish list item.
Refinements of a concept can only add restrictions, they cannot remove them.
True. But it's not hard to define the concepts in such a way that the lock classes can have different constructors:
Lock concept defines all lock operations except constructors. TryLock concept refines Lock by adding try_* methods. TimedLock concept refines TryLock by adding timed_* methods.
ScopedLock refines Lock by adding appropriate constructors. ScopedTryLock refines TryLock by adding appropriate constructors. ScopedTimedLock refines TimedLock by adding appropriate constructors.
This isn't necessary, because the Lock concept that all three currently refine from does not define any constructors. Lock concept defines basic lock operations and destructor. ScopedLock refines Lock by adding constructors. ScopedTryLock refines Lock by adding constructors and the try_lock method(). ScopedTimedLock refines Lock by adding constructors and the timed_lock method(). But it looks as if the original concepts are not as generic as I may have hoped, at least on construction. Perhaps this is the right answer, as locks are (according to the docs) supposed to be shorted lived objects. -- Christopher Currie <codemonkey@gmail.com>

Batov, Vladimir wrote:
Guys,
Could we please resist complicating interfaces when we do not have to? I personally consider an additional blocking parameter for something like try_lock excessive because it distorts try_lock into becoming scoped_lock. Let's keep different concepts different. Blocking in try_lock is an oxymoron -- try_lock cannot block because it is "try".
Perhaps I would agree, except that try_lock already has a lock() method, which, by design, blocks. To rephrase what Christopher pointed out, a scoped_try_lock is nearly the same as a scoped_lock with try_* methods added (I say nearly because the constructors are different, which is partly what I'm trying to address).
The basic usages (covering about 80%) are:
scoped_lock l(m); // Block until it locks. scoped_try_lock l(m); // Try (!) to lock. I'll check the result. scoped_timed_lock l(m, time); // Block until it locks or time runs out. I'll check the result.
Simple and clean.
True. It's also simple and clean to say that a try_lock is a lock with added functionality, and a timed_lock is a try_lock with added functionality.
The only functionality that I can see beyond that is when I have to only declare a lock but apply it later. Like:
Void Foo() { scoped_lock l(m, do_not_lock);
if (something) { l.lock(); // Can't declare here. ... } } For that purpose we'll need something like
scoped_lock(mutex&, nolock_t);
I'm increasingly liking this syntax, by the way; thanks for suggesting it.
That's it. Isn't it?
Alternatively we might consider making timed_lock our basic lock as scoped_lock and try_lock are merely border-cases (time=infinite and time=0 respectively). Then, scoped_lock and try_lock would be real refinements (convenience interfaces) of that general concept.
Mike

On Tuesday 29 June 2004 9:15 am, Michael Glassford wrote:
For that purpose we'll need something like
scoped_lock(mutex&, nolock_t);
I'm increasingly liking this syntax, by the way; thanks for suggesting it.
I don't know the original intention, but my assumption about the "bool" parameter was that it was intended to be used to check (run-time) conditions that might eliminate the need for locking under certain circumstances, e.g., bool foo(bool threadsafe) { mutex::scoped_lock l(m, !threadsafe); } Using types such as nolock_t would prevent such a usage. Doug

Doug Gregor wrote:
On Tuesday 29 June 2004 9:15 am, Michael Glassford wrote:
For that purpose we'll need something like
scoped_lock(mutex&, nolock_t);
I'm increasingly liking this syntax, by the way; thanks for suggesting it.
I don't know the original intention, but my assumption about the "bool" parameter was that it was intended to be used to check (run-time) conditions that might eliminate the need for locking under certain circumstances, e.g.,
bool foo(bool threadsafe) { mutex::scoped_lock l(m, !threadsafe); }
Using types such as nolock_t would prevent such a usage.
I hadn't considered that, but I guess I'll have to now. Mike

Michael Glassford wrote:
Doug Gregor wrote:
On Tuesday 29 June 2004 9:15 am, Michael Glassford wrote:
For that purpose we'll need something like
scoped_lock(mutex&, nolock_t);
I'm increasingly liking this syntax, by the way; thanks for suggesting it.
I don't know the original intention, but my assumption about the "bool" parameter was that it was intended to be used to check (run-time) conditions that might eliminate the need for locking under certain circumstances, e.g.,
bool foo(bool threadsafe) { mutex::scoped_lock l(m, !threadsafe); }
Using types such as nolock_t would prevent such a usage.
I hadn't considered that, but I guess I'll have to now.
Would it be sufficient that the above use case would be supported like this: bool foo(bool threadsafe) { mutex::scoped_lock l(m, unlocked); if (!threadsafe) l.lock(); } ? Mike
participants (15)
-
Alexander Terekhov
-
alnsn@yandex.ru
-
Anthony Williams
-
Batov, Vladimir
-
Bruno Martínez Aguerre
-
Christopher Currie
-
David Abrahams
-
Doug Gregor
-
Eric Niebler
-
Howard Hinnant
-
Howard Hinnant
-
Matt Hurd
-
Matthew Vogt
-
Michael Glassford
-
Peter Dimov