Boost.Threads: Do we need all those mutexes?

Do we need all three separate concepts -- mutex, try_mutex and timed_mutex? I'd expect the mutex concept to remain unchanged and "try" and "timed" refinements to be related solely to the ways I try locking that only mutex. That is, I always have one mutex and under different circumstances lock it unconditionally (scoped_lock) or try locking it and check if that attempt succeeds (scoped_try_lock) or try locking within a certain time period (scoped_timed_lock).

Batov, Vladimir wrote:
Do we need all three separate concepts -- mutex, try_mutex and timed_mutex?
I think the idea was that it would be possible to implement the mutex types that supported fewer operations more efficiently. Whether this actually turned out to be true is another question.
I'd expect the mutex concept to remain unchanged and "try" and "timed" refinements to be related solely to the ways I try locking that only mutex. That is, I always have one mutex and under different circumstances lock it unconditionally (scoped_lock) or try locking it and check if that attempt succeeds (scoped_try_lock) or try locking within a certain time period (scoped_timed_lock).
If you want this, just use a timed_mutex and scoped_timed_lock. In the current release it supports lock() and timed_lock() operations; in the next release it will support try_lock() as well. Mike

So, would anyone care to summarize their take on what conclusions have been reached? I'm afraid I haven't had time in the last few days to respond much, or even to analyze everything as much as I would have liked; and in many cases, it's not obvious what conclusions were reached, if any. As far as I remember, the following topics have come up; undoubtedly I've missed something, so please add it to the list. Lock unification ---------------- Should the three lock types (lock, try lock, timed lock) be combined into one? Conclusion: the lock class should be implemented as a single class templated on the mutex type; trying to use operations that the mutex doesn't support results in a compile error. Question: does this mean unification of the lock concepts, too? Extended lock interface ----------------------- The lock interface should be extended to make it possible to determine if it is locking a particular mutex. How? Suggestions: Mutex& mutex() Mutex* mutex() void* mutex() mutex_id_type mutex() Conclusion: ??? Mutex/lock interface specification ---------------------------------- The private mutex interface used by locks should be specified so that others can write custom mutex classes that work with the Boost.Threads lock class. Mutex unification ----------------- Should the three mutex types (mutex, try mutex, timed mutex) be combined into one? Conclusion: I think it was concluded that a separate timed mutex is necessary. Possibly that the mutex and try mutex should be combined. That this combined mutex should optionally provide timed operations if they can be supported efficiently by the underlying platform. Read/Write lock demotion ------------------------ What should the lock demotion syntax look like? Conclusion: ??? Read/Write lock promotion ------------------------- Should lock promotion be supported or is it too problematic? Should there be failable promotion, promotion that uses upgradeable locks and doesn't fail, or both? What should the promotion syntax look like? Conclusion: ??? Other miscellaneous topics -------------------------- * The Boost.Threads implementation of a condition variable waiting on a recursive mutex that is locked more than once is buggy. * The very idea of a condition variable waiting on a recursive mutex that is locked more than once is a bad idea. Topics that were postponed -------------------------- * Making locks movable * Supporting "if (Lock l = ...)" syntax * Dealing with how time is specified in timed lock operations. Mike

Michael Glassford wrote:
Question: does this mean unification of the lock concepts, too?
This is a very interesting question. At first sight the answer seems a trivial "no" because, strictly speaking, the lock template still does describe three concepts. What is interesting is whether we need lock concepts at all. In Boost.Threads, a lock concept is only needed (I believe) in condition::wait. But condition::wait is not actually generic. It only pretends to work with any class that is-a Lock, but in reality, it requires one of Boost.Threads locks. Note that the question is not whether the world needs lock concepts. The question is whether it is our responsibility to define them, if we don't actually use them anywhere. A related question is: given a mutex M, is the lock type obtainable via M::scoped_lock, as before, or is it simply scoped_lock<M>? Given my preference for simplifying as much as possible (but no further), you could probably tell how I'd answer the two questions above. ;-)

Michael Glassford wrote:
So, would anyone care to summarize their take on what conclusions have been reached? I'm afraid I haven't had time in the last few days to respond much, or even to analyze everything as much as I would have liked; and in many cases, it's not obvious what conclusions were reached, if any. As far as I remember, the following topics have come up; undoubtedly I've missed something, so please add it to the list.
The volume of messages with the original topic has become hard (for me personally) to track, so I'll address each issue in a separate thread.
Lock unification ---------------- Should the three lock types (lock, try lock, timed lock) be combined into one?
Conclusion: the lock class should be implemented as a single class templated on the mutex type; trying to use operations that the mutex doesn't support results in a compile error.
Question: does this mean unification of the lock concepts, too?
I've missed some of the arguments here, so can someone write up a good rationale for this change? I'm not sure I'm convinced it's necessary. I find that having separate types makes the sort of locking you're trying to do more explicit and readable. The differences between { scoped_lock l( m ); } and { scoped_timed_lock l( m, t ); } are much more descriptive me than { scoped_lock( m ); } { scoped_lock( m, t ); } which would seem to require some implicit knowledge of the constructor arguments. I can infer that the apparent justification for unification would be to allow a single lock to be used in different ways, but I have a hard time imagining a use case where this would be as clear and as error-resistant as separate scopes with separate locks. If anyone has one, or any other rationale, please respond. -- Christopher Currie <codemonkey@gmail.com>

Christopher Currie wrote:
Michael Glassford wrote:
So, would anyone care to summarize their take on what conclusions have been reached? I'm afraid I haven't had time in the last few days to respond much, or even to analyze everything as much as I would have liked; and in many cases, it's not obvious what conclusions were reached, if any. As far as I remember, the following topics have come up; undoubtedly I've missed something, so please add it to the list.
The volume of messages with the original topic has become hard (for me personally) to track, so I'll address each issue in a separate thread.
Lock unification ---------------- Should the three lock types (lock, try lock, timed lock) be combined into one?
Conclusion: the lock class should be implemented as a single class templated on the mutex type; trying to use operations that the mutex doesn't support results in a compile error.
Question: does this mean unification of the lock concepts, too?
I've missed some of the arguments here, so can someone write up a good rationale for this change? I'm not sure I'm convinced it's necessary. I find that having separate types makes the sort of locking you're trying to do more explicit and readable. The differences between
{ scoped_lock l( m ); }
and
{ scoped_timed_lock l( m, t ); }
are much more descriptive me than
{ scoped_lock( m ); }
{ scoped_lock( m, t ); }
Actually, it has been suggested to eliminate the latter constructor that takes a time as a parameter. There would only be one constructor: scoped_lock(Mutex m&, bool initially_locked=true); which performs a blocking lock. Under this scheme, you would have to write: scoped_lock l(m, false); l.timed_lock(t);
which would seem to require some implicit knowledge of the constructor arguments.
I can infer that the apparent justification for unification would be to allow a single lock to be used in different ways, but I have a hard time imagining a use case where this would be as clear and as error-resistant as separate scopes with separate locks. If anyone has one, or any other rationale, please respond.
The main motivation behind the idea of unifying the lock types (and mutex types) is simplification of concepts, I believe. Mike

Michael Glassford wrote:
Actually, it has been suggested to eliminate the latter constructor that takes a time as a parameter. There would only be one constructor:
scoped_lock(Mutex m&, bool initially_locked=true);
which performs a blocking lock. Under this scheme, you would have to write:
scoped_lock l(m, false); l.timed_lock(t);
I personally think this makes the interface more complicated, but I guess that's a matter of opinion. I liken 'lock-on-construction' to 'resource-acquisition-is-initialization', but I can see how a timed-lock may not precicely model this concept.
The main motivation behind the idea of unifying the lock types (and mutex types) is simplification of concepts, I believe.
What concerns me is that it feels like the concepts are being loosened. By combining the lock concepts, you effective remove the ability to demand that a lock always be blocking, regardless of the type of mutex. I no longer have the option of saying: template <typename mutex_type> foo( mutex_type & m ) { mutex_type::scoped_lock l( m ); // do work l.unlock(); // do more l.try_lock(); // compile time error? // still more } If the locks are combined, whether this is an error is dependent on if the mutex supports a try_lock operation, whereas with the existing interface, this is always an error, because I've said up front that I only ever want to perform blocking locks here. I'll also point out that the existing interface supports the opposite desire, a compile time error based on mutex_type: template <typename mutex_type> foo( mutex_type & m ) { // compile time error possible mutex_type::scoped_try_lock l( m ); // do work l.unlock(); // do more l.try_lock(); // ok // still more } This will compile if a TryMutex is passed, but not a plain Mutex, because scoped_try_lock is not defined for that type. In the end, I feel that by combining the interfaces, we take away the ability of the user to be explicit and restrictive in her use of lock types. My other motivation is an "if it ain't broke, don't fix it" attitude; Unless it can be demonstrated that a change in the interface is necessary to support needed use cases, we may as well leave well enough alone (and devote time to adding new features :-)). -- Christopher Currie <codemonkey@gmail.com>

Christopher Currie wrote:
Michael Glassford wrote:
Actually, it has been suggested to eliminate the latter constructor that takes a time as a parameter. There would only be one constructor:
scoped_lock(Mutex m&, bool initially_locked=true);
which performs a blocking lock. Under this scheme, you would have to write:
scoped_lock l(m, false); l.timed_lock(t);
I personally think this makes the interface more complicated, but I guess that's a matter of opinion. I liken 'lock-on-construction' to 'resource-acquisition-is-initialization', but I can see how a timed-lock may not precicely model this concept.
Actually, if you've been following the whole discussion, you'll know that I agree with you. My original intent was to add constructors that would make it explicit whether you were asking for a blocking lock, a try lock, or a timed lock, and to remove the ambiguity in the try_lock constructors (rather arbitrarily, one is blocking, the other non-blocking, even though they look just like the lock class constructors which both block). There were some who argued against my idea and none who supported it, so I dropped the it. I don't know if this current discussion grew out of that or if it just happened to come up about the same time; but anyway, it happened. Having said all that, I should mention that I see the arguments for simplification, too, and that I prefer either alternative to the way things are now. I should also mention that very few of the discussed changes is likely to be in the upcoming release--the discussion has been too involved and the time frame is too short. It would be good to get a wider range of viewpoints, too--the discussion so far has involved too few people, in my opinion.
The main motivation behind the idea of unifying the lock types (and mutex types) is simplification of concepts, I believe.
What concerns me is that it feels like the concepts are being loosened. By combining the lock concepts, you effective remove the ability to demand that a lock always be blocking, regardless of the type of mutex. I no longer have the option of saying:
Actually, it has been true all along that (except for constructors) a try lock is a superset of a plain lock, and a timed lock was nearly a superset of a try lock. I wanted to (and have in the upcoming release) made timed lock an actual superset of a try lock by adding a try_lock() member function to it.
template <typename mutex_type> foo( mutex_type & m ) { mutex_type::scoped_lock l( m );
// do work
l.unlock();
// do more
l.try_lock(); // compile time error?
// still more }
If the locks are combined, whether this is an error is dependent on if the mutex supports a try_lock operation, whereas with the existing interface, this is always an error, because I've said up front that I only ever want to perform blocking locks here.
I'll also point out that the existing interface supports the opposite desire, a compile time error based on mutex_type:
template <typename mutex_type> foo( mutex_type & m ) { // compile time error possible mutex_type::scoped_try_lock l( m );
// do work
l.unlock();
// do more
l.try_lock(); // ok
// still more }
This will compile if a TryMutex is passed, but not a plain Mutex, because scoped_try_lock is not defined for that type.
Of course this would be true with the single-lock scheme also.
In the end, I feel that by combining the interfaces, we take away the ability of the user to be explicit and restrictive in her use of lock types.
So would you advocate removing the lock() method and the blocking lock constructors from try_lock and timed_lock? I think the three most consistent designs would be: 1) No overlap between lock, try_lock, and timed_lock. 2) Timed_lock is superset of try_lock, which is a superset of lock. 3) Only one templated lock class that does everything if the mutex supports it. The current situation is none of the above.
My other motivation is an "if it ain't broke, don't fix it" attitude; Unless it can be demonstrated that a change in the interface is necessary to support needed use cases, we may as well leave well enough alone
Thanks for your comments. Some of them represent a viewpoint no one has expressed yet, which is in my opinion a good thing.
(and devote time to adding new features :-)).
Then you'll be glad to know that most of my Boost time is being devoted to adding new features (mostly features at least partially implemented by the original Boost.Threads author before he stopped working on it). See http://tinyurl.com/4wdtz (note that it will be slightly out of date until the page is refreshed). Mike

Michael Glassford wrote:
Actually, if you've been following the whole discussion, you'll know that I agree with you.
My appologies, I lost track of the thread a while back, and haven't had the time to catch up.
try lock, or a timed lock, and to remove the ambiguity in the try_lock constructors (rather arbitrarily, one is blocking, the other non-blocking, even though they look just like the lock class constructors which both block).
I'm on the fence on this issue; since it's been put off until after the next release I won't go into it here.
Actually, it has been true all along that (except for constructors) a try lock is a superset of a plain lock, and a timed lock was nearly a superset of a try lock. I wanted to (and have in the upcoming release) made timed lock an actual superset of a try lock by adding a try_lock() member function to it.
I would actually prefer that timed_lock *not* be a superset of try_lock. The documented concepts clearly don't imply that, it appears to be an artifact of the implementation. If you happen to want to use try_lock semantics on a timed_lock, it's easy enough to use a zero timeout.
template <typename mutex_type> foo( mutex_type & m ) { mutex_type::scoped_lock l( m );
// do work
l.unlock();
// do more
l.try_lock(); // compile time error?
// still more }
If the locks are combined, whether this is an error is dependent on if the mutex supports a try_lock operation, whereas with the existing interface, this is always an error, because I've said up front that I only ever want to perform blocking locks here.
I'll also point out that the existing interface supports the opposite desire, a compile time error based on mutex_type:
template <typename mutex_type> foo( mutex_type & m ) { // compile time error possible mutex_type::scoped_try_lock l( m );
// do work
l.unlock();
// do more
l.try_lock(); // ok
// still more }
This will compile if a TryMutex is passed, but not a plain Mutex, because scoped_try_lock is not defined for that type.
Of course this would be true with the single-lock scheme also.
True, it's the first case that doesn't appear to be possible with the single-lock scheme. In otherwords, I want the *lock* to prohibit the try_lock operation, not the mutex.
In the end, I feel that by combining the interfaces, we take away the ability of the user to be explicit and restrictive in her use of lock types.
So would you advocate removing the lock() method and the blocking lock constructors from try_lock and timed_lock? I think the three most consistent designs would be:
1) No overlap between lock, try_lock, and timed_lock.
As it stands, I feel I would advocate this option, at least among constructors, perhaps leaving the lock() method intact. Though I wouldn't cry if it were taken away, because it isn't strictly necessary, just a convenience.
Then you'll be glad to know that most of my Boost time is being devoted to adding new features (mostly features at least partially implemented by the original Boost.Threads author before he stopped working on it). See http://tinyurl.com/4wdtz (note that it will be slightly out of date until the page is refreshed).
Thanks to you, for picking up the reigns to maintain it. I know you've done a lot of work to ensure that Boost.Threads has not grown stagnant, and matures. Christopher -- Christopher Currie <codemonkey@gmail.com>

On Jul 15, 2004, at 2:58 PM, Michael Glassford wrote:
My original intent was to add constructors that would make it explicit whether you were asking for a blocking lock, a try lock, or a timed lock, and to remove the ambiguity in the try_lock constructors (rather arbitrarily, one is blocking, the other non-blocking, even though they look just like the lock class constructors which both block). There were some who argued against my idea and none who supported it, so I dropped the it.
Fwiw, I'm still toying with: lock(m, not_locked); lock(m, not_blocked); (or whatever the spelling was). I haven't been using that syntax in my examples simply because I don't have it coded that way and I try to compile my example code before I post it (at least usually). Anyway, I'm personally not ready to drop the enum set you proposed, or at least a subset of it. There's a lot of stuff that's been proposed. Perhaps we need competing implementations that can be played with (as opposed to just competing designs). -Howard

Howard Hinnant wrote:
On Jul 15, 2004, at 2:58 PM, Michael Glassford wrote:
My original intent was to add constructors that would make it explicit whether you were asking for a blocking lock, a try lock, or a timed lock, and to remove the ambiguity in the try_lock constructors (rather arbitrarily, one is blocking, the other non-blocking, even though they look just like the lock class constructors which both block). There were some who argued against my idea and none who supported it, so I dropped the it.
Fwiw, I'm still toying with:
lock(m, not_locked); lock(m, not_blocked);
(or whatever the spelling was). I haven't been using that syntax in my examples simply because I don't have it coded that way and I try to compile my example code before I post it (at least usually). Anyway, I'm personally not ready to drop the enum set you proposed, or at least a subset of it.
OK, thanks for the feedback. For what it's worth, then, the latest form of the proposal (which I'm not sure I ever posted) looked like this: namespace lock_state { typedef enum { unlocked=0, locked=1 } lock_state; } //namespace 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, blocking_mode) //always locking ScopedTimedLock / ScopedTimedReadLock / ScopedTimedWriteLock ------------------------------------------------------------ timed_lock(M) //always locking, blocking timed_lock(M, lock_state) //blocking timed_lock(M, blocking_mode) //always locking timed_lock(M, t) //always locking, blocking for time t
There's a lot of stuff that's been proposed. Perhaps we need competing implementations that can be played with (as opposed to just competing designs).
Mike

Christopher Currie wrote:
The differences between
{ scoped_lock l( m ); }
and
{ scoped_timed_lock l( m, t ); }
are much more descriptive me than
{ scoped_lock( m ); }
{ scoped_lock( m, t ); }
The "unified" timed lock case is { scoped_lock l( m, false ); if( l.timed_lock( t ) ) { // do stuff } else { // what do we do here? presumably either: // setup for unbounded wait l.lock(); // do stuff // or signal failure } } The scoped_timed_lock use case also needs to eventually contain an if( l ) statement; you can't do much if you don't know whether your timed lock succeeded. One might also argue that a separate scoped_timed_lock makes accidentally omitting the if() statement more likely and harder to spot. But it's hard to tell without real use cases.

Peter Dimov wrote:
The "unified" timed lock case is
{ scoped_lock l( m, false );
if( l.timed_lock( t ) ) { // do stuff } else { // what do we do here? presumably either:
// setup for unbounded wait l.lock(); // do stuff
// or signal failure } }
The scoped_timed_lock use case also needs to eventually contain an if( l ) statement; you can't do much if you don't know whether your timed lock succeeded. One might also argue that a separate scoped_timed_lock makes accidentally omitting the if() statement more likely and harder to spot.
But it's hard to tell without real use cases.
I continue to believe that a better interface for a unified lock involves descriptively named helper functions: scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock It's just as clear as you could hope for, and it's extensible. I feel that the single lock c'tor with a bool arg is less readable and rather inflexible. Earlier I posted code demonstrating how this interface could be made to work. -- Eric Niebler Boost Consulting www.boost-consulting.com

"Eric Niebler" <eric@boost-consulting.com> writes:
I continue to believe that a better interface for a unified lock involves descriptively named helper functions:
scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock
It's just as clear as you could hope for,
I'm not convinced it is. It potentially puts information about the kind of locking being done very far from the actual call that does locking.
and it's extensible. I feel that the single lock c'tor with a bool arg is less readable and rather inflexible.
Inflexible how? -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote:
"Eric Niebler" <eric@boost-consulting.com> writes:
I continue to believe that a better interface for a unified lock involves descriptively named helper functions:
scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock
It's just as clear as you could hope for,
I'm not convinced it is. It potentially puts information about the kind of locking being done very far from the actual call that does locking.
True, but the other design under consideration doesn't help with that. It requires two-step construction for some constructs which could result in code like: scoped_lock l( m, false ); maybe_takes_the_lock_i_dont_know( l );
and it's extensible. I feel that the single lock c'tor with a bool arg is less readable and rather inflexible.
Inflexible how?
User's can't add more constructors if they want a different locking strategy, but they can add as many helper function as they like. For instance, they could define a function that tried to take a timed_lock and logs failures. Or throws an exception. Or they could write a lock_if helper that takes a predicate. And they can do all that with simple one-step construction syntax. It's more concise and it permits the lock declaration to appear in the condition of if statements. -- Eric Niebler Boost Consulting www.boost-consulting.com

"Eric Niebler" <eric@boost-consulting.com> writes:
David Abrahams wrote:
"Eric Niebler" <eric@boost-consulting.com> writes:
I continue to believe that a better interface for a unified lock involves descriptively named helper functions:
scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock
It's just as clear as you could hope for, I'm not convinced it is. It potentially puts information about the kind of locking being done very far from the actual call that does locking.
True, but the other design under consideration doesn't help with that. It requires two-step construction for some constructs which could result in code like:
scoped_lock l( m, false ); maybe_takes_the_lock_i_dont_know( l );
I don't know what you're referring to. I don't see any two-step construction going on there. Links or examples, please?
and it's extensible. I feel that the single lock c'tor with a bool arg is less readable and rather inflexible. Inflexible how?
User's can't add more constructors if they want a different locking strategy, but they can add as many helper function as they like. For instance, they could define a function that tried to take a timed_lock and logs failures. Or throws an exception. Or they could write a lock_if helper that takes a predicate. And they can do all that with simple one-step construction syntax. It's more concise and it permits the lock declaration to appear in the condition of if statements.
Sorry, I missed the "single lock ctor" part. I think I agree with you there. BTW, it just occurred to me that timed_lock(m, 0) could potentially generate different code from timed_lock(m, some_int) If we can detect literal zero... which I think is possible. What about: scoped_lock l(m); // block scoped_lock l(m, 0); // try scoped_lock l(m, 33); // timed scoped_lock l(m, deferred); // deferred scoped_lock l(deferred(m)); // alternate ?? -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote:
"Eric Niebler" <eric@boost-consulting.com> writes:
David Abrahams wrote:
"Eric Niebler" <eric@boost-consulting.com> writes:
I continue to believe that a better interface for a unified lock involves descriptively named helper functions:
scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock
It's just as clear as you could hope for,
I'm not convinced it is. It potentially puts information about the kind of locking being done very far from the actual call that does locking.
True, but the other design under consideration doesn't help with that. It requires two-step construction for some constructs which could result in code like:
scoped_lock l( m, false ); maybe_takes_the_lock_i_dont_know( l );
I don't know what you're referring to. I don't see any two-step construction going on there. Links or examples, please?
With the single c'tor approach that Peter D. has been championing, to get a try lock, you need two statements: scoped_lock l( m, false ); if( l.try_lock() ) ... That's all I was refering to. Perhaps "2-step construction" is misleading. My point was, it's just as easy to make the locking logic remote with this design as with mine.
BTW, it just occurred to me that
timed_lock(m, 0)
could potentially generate different code from
timed_lock(m, some_int)
If we can detect literal zero... which I think is possible.
What about:
scoped_lock l(m); // block scoped_lock l(m, 0); // try scoped_lock l(m, 33); // timed
scoped_lock l(m, deferred); // deferred scoped_lock l(deferred(m)); // alternate
??
Interesting. That's workable. -- Eric Niebler Boost Consulting www.boost-consulting.com

Eric Niebler wrote:
David Abrahams wrote:
BTW, it just occurred to me that
timed_lock(m, 0)
could potentially generate different code from
timed_lock(m, some_int)
If we can detect literal zero... which I think is possible.
What about:
scoped_lock l(m); // block scoped_lock l(m, 0); // try scoped_lock l(m, 33); // timed
scoped_lock l(m, deferred); // deferred scoped_lock l(deferred(m)); // alternate
??
Interesting. That's workable.
On second thought, I think it's too subtle. You can detect literal zero, but you can't detect (at compile-time) an int with a value of zero. I'm not comfortable with this: scoped_lock l(m, 0); meaning something different than: int t = 0; scoped_lock l(m, t); Perhaps it's really OK because the effect is the same, but the fact that they would execute different code paths sets off bells in my head. -- Eric Niebler Boost Consulting www.boost-consulting.com

Eric Niebler wrote:
David Abrahams wrote:
BTW, it just occurred to me that <snip> On second thought, I think it's too subtle. You can detect literal zero, but you can't detect (at compile-time) an int with a value of zero. I'm not comfortable with this:
scoped_lock l(m, 0);
meaning something different than:
int t = 0; scoped_lock l(m, t);
Perhaps it's really OK because the effect is the same, but the fact that they would execute different code paths sets off bells in my head.
Perhaps the interface should be lock lk<0>(m); lock lk<33>(m); lock lk(m); to support specialization of zero. I can see a case for allowing both to allow run-time discrimination... needs more thought. Matt Hurd.

"Eric Niebler" <eric@boost-consulting.com> writes:
Eric Niebler wrote:
David Abrahams wrote:
BTW, it just occurred to me that
timed_lock(m, 0)
could potentially generate different code from
timed_lock(m, some_int)
If we can detect literal zero... which I think is possible.
What about:
scoped_lock l(m); // block scoped_lock l(m, 0); // try scoped_lock l(m, 33); // timed
scoped_lock l(m, deferred); // deferred scoped_lock l(deferred(m)); // alternate ??
Interesting. That's workable.
On second thought, I think it's too subtle. You can detect literal zero, but you can't detect (at compile-time) an int with a value of zero. I'm not comfortable with this:
scoped_lock l(m, 0);
meaning something different than:
int t = 0; scoped_lock l(m, t);
Perhaps it's really OK because the effect is the same, but the fact that they would execute different code paths sets off bells in my head.
I'm not too worried. It really would be semantically identical and the timed lock code could contain a branch for the zero case if you want efficiency. It might still be too cute, but as long as we're talking about slimming the interface down we should discuss it. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote:
I'm not too worried. It really would be semantically identical and the timed lock code could contain a branch for the zero case if you want efficiency.
It might still be too cute, but as long as we're talking about slimming the interface down we should discuss it.
This implicitly assumes relative timeouts.

"Peter Dimov" <pdimov@mmltd.net> writes:
David Abrahams wrote:
I'm not too worried. It really would be semantically identical and the timed lock code could contain a branch for the zero case if you want efficiency.
It might still be too cute, but as long as we're talking about slimming the interface down we should discuss it.
This implicitly assumes relative timeouts.
As opposed to what? Maybe I don't understand the term. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote:
"Peter Dimov" <pdimov@mmltd.net> writes:
David Abrahams wrote:
I'm not too worried. It really would be semantically identical and the timed lock code could contain a branch for the zero case if you want efficiency.
It might still be too cute, but as long as we're talking about slimming the interface down we should discuss it.
This implicitly assumes relative timeouts.
As opposed to what? Maybe I don't understand the term.
As opposed to absolute timeouts (either rooted on 1970-1-1 00:00:00 UTC or monotonic). Although, now that I think of it, zero does mean a try lock in all of these, so my statement is wrong.

"Peter Dimov" <pdimov@mmltd.net> writes:
David Abrahams wrote:
"Peter Dimov" <pdimov@mmltd.net> writes:
David Abrahams wrote:
I'm not too worried. It really would be semantically identical and the timed lock code could contain a branch for the zero case if you want efficiency.
It might still be too cute, but as long as we're talking about slimming the interface down we should discuss it.
This implicitly assumes relative timeouts.
As opposed to what? Maybe I don't understand the term.
As opposed to absolute timeouts (either rooted on 1970-1-1 00:00:00 UTC or monotonic). Although, now that I think of it, zero does mean a try lock in all of these, so my statement is wrong.
Regardless, if our interface is going to support timed locks, don't we need to choose one of these schemes? -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

On Thu, 15 Jul 2004 17:23:40 -0400, David Abrahams <dave@boost-consulting.com> wrote:
What about:
scoped_lock l(m); // block scoped_lock l(m, 0); // try scoped_lock l(m, 33); // timed
scoped_lock l(m, deferred); // deferred scoped_lock l(deferred(m)); // alternate
Seems neat. A deferred timed lock make sense, but not in this context you are deferring, I assume, the locking strategy rather than the lock, or do I have that wrong... it's been a long threading thread ;-) I'm not sure I see the case for deferring anything a stack based scoped lock with a: void folly () { { scoped_lock l(m); do_stuff(); } { scoped_lock l(m); do_more_stuff(); } } is still preferable to me than allowing explicit locking of a lock. I worry about paying for the overhead, perhaps in space for a scoped time lock that records it timing versus a blocking lock that needs no such information, but I'm not sure that it is a big deal as the difference is negligible and these will typically be stack based. Implementation will reveal all I guess. As I've mentioned previously, it would be nice to consider this interface in the general sense of the two concepts: 1) resource 2) acquisition As this is equally applicable to mutex, lock and socket, packet, etc... Perhaps such a generalization makes the explicit locking of a lock more worthwhile?? I'd have to see the code... Regards, Matt Hurd

On Jul 15, 2004, at 7:42 PM, Matt Hurd wrote:
I'm not sure I see the case for deferring anything a stack based scoped lock with a: void folly () { { scoped_lock l(m); do_stuff(); }
{ scoped_lock l(m); do_more_stuff(); } } is still preferable to me than allowing explicit locking of a lock.
I have a couple of places where I need a "deferred" lock: Namely I have 2 generic templated lock classes that take other locks as template parameters, and store references to those locks: template <class Lock1, class Lock2> class transfer_lock; template <class Lock1, class Lock2> class lock_both; The constructor for transfer_lock takes two locks, referencing the same mutex, the first of which must be unlocked, and the second locked. The constructor for lock_both takes two locks, referencing different mutexes, both of which must be unlocked. Example use: mutex m; scoped_lock lock1(m1, deferred); // or whatever syntax for not locking scoped_lock lock2(m2, deferred); lock_both<scoped_lock, scoped_lock> lock(lock1, lock2); // m1 and m2 atomically locked here, without fear of deadlock Without the ability to construct but defer locking of lock1 and lock2, the construction of lock_both becomes inefficient and awkward.
I worry about paying for the overhead, perhaps in space for a scoped time lock that records it timing versus a blocking lock that needs no such information, but I'm not sure that it is a big deal as the difference is negligible and these will typically be stack based. Implementation will reveal all I guess.
There is no overhead in the lock types, at least with the current boost design. All locks I've coded contain a reference to the mutex, and a bool indicating locked status (even the read_lock, write_lock and upgradable_read_lock). But there is overhead in the mutex types, and it can vary significantly among the different types: (basic, try, timed, recursive, read/write). Sometimes the overhead is on the stack, sometimes not (depends on the OS and on the mutex implementation). Here is a sample survey of sizeof() on one of my platforms I need to support: sizeof(mutex) = 44 sizeof(try_mutex) = 44 sizeof(timed_mutex) = 76 sizeof(recursive_mutex) = 44 sizeof(recursive_try_mutex) = 44 sizeof(recursive_timed_mutex) = 80 sizeof(rw_mutex) = 108 On another platform it looks like: sizeof(mutex) = 44 sizeof(try_mutex) = 44 sizeof(timed_mutex) = 76 sizeof(recursive_mutex) = 80 sizeof(recursive_try_mutex) = 80 sizeof(recursive_timed_mutex) = 80 sizeof(rw_mutex) = 108 On a third platform I support there are differences between mutex and try_mutex, but the difference is not well illustrated by sizeof() because the "mutex type" is just a 4 byte handle in some cases (but not others). sizeof(mutex) = 24 sizeof(try_mutex) = 4 sizeof(timed_mutex) = 4 sizeof(recursive_mutex) = 24 sizeof(recursive_try_mutex) = 4 sizeof(recursive_timed_mutex) = 4 sizeof(rw_mutex) = not implemented And of course all of this is subject to change. But if I'm forced to include all capability into one mutex type, then there is definitely going to be a price paid on some platforms by those who only want a mutex (which is also by far the most often needed type). -Howard

On Thu, 15 Jul 2004 22:30:37 -0400, Howard Hinnant <hinnant@twcny.rr.com> wrote:
On Jul 15, 2004, at 7:42 PM, Matt Hurd wrote:
I'm not sure I see the case for deferring anything a stack based scoped lock with a: void folly () { { scoped_lock l(m); do_stuff(); }
{ scoped_lock l(m); do_more_stuff(); } } is still preferable to me than allowing explicit locking of a lock.
I have a couple of places where I need a "deferred" lock: Namely I have 2 generic templated lock classes that take other locks as template parameters, and store references to those locks:
template <class Lock1, class Lock2> class transfer_lock; template <class Lock1, class Lock2> class lock_both;
The constructor for transfer_lock takes two locks, referencing the same mutex, the first of which must be unlocked, and the second locked.
In principle, tranferring the true from one bool to another sounds a nice thing to do, however: I'm not sure this is a good idea. For a single thread this could probably be worked out with an alternative design / refactor. Transferring locks between threads might be desirable, but could introduce a bevy of complexities for other things as usually a mutex lock is assumed to be associated with a particular thread. For example, we have talked about a vector of mutexes and supporting locking of a collection where a mutex is associated, potentially, with many objects. Transfer lock would complicate this as you need to map thread to object for some models that allow recursive locking for the same thread to avoid deadlock. You might be right though, I'm a slave to efficiency. Do you have a specific use case in mind?
The constructor for lock_both takes two locks, referencing different mutexes, both of which must be unlocked.
Example use:
mutex m; scoped_lock lock1(m1, deferred); // or whatever syntax for not locking scoped_lock lock2(m2, deferred); lock_both<scoped_lock, scoped_lock> lock(lock1, lock2); // m1 and m2 atomically locked here, without fear of deadlock
Without the ability to construct but defer locking of lock1 and lock2, the construction of lock_both becomes inefficient and awkward.
My immediate reaction was to think this was a bad idea, but a moment later I'm not so sure. My initial thought, and I think it is still valid, was that locking many things atomically doesn't make sense. You have to lock them one after the other and always in the same order to prevent deadlock. Perhaps you have a scheme in mind where you try lock for all things and release if you can't get all and then try again causing a group spin lock? I think I'm missing a part of your picture, let me know what you're thinking. If I forget about the atomicity aspect then locking groups of things does make sense. Now, consider the locking of many mutexes (mutexi? ;-) ). I'm not sure passing deferred locks to the collection, in your case a two lock collection, is the best approach. Something like scoped_multi_lock lk( mutex_vector ); or scoped_multi_lock lk( m1, m2 ); or scoped_multi_lock lk( some_mpl_mutex_vector_thing ); might work just as well. Specification of ordering for locking the mutexes is another matter. Very interested in your thoughts on this.
I worry about paying for the overhead, perhaps in space for a scoped time lock that records it timing versus a blocking lock that needs no such information, but I'm not sure that it is a big deal as the difference is negligible and these will typically be stack based. Implementation will reveal all I guess.
There is no overhead in the lock types, at least with the current boost design. All locks I've coded contain a reference to the mutex, and a bool indicating locked status (even the read_lock, write_lock and upgradable_read_lock). But there is overhead in the mutex types, and it can vary significantly among the different types: (basic, try, timed, recursive, read/write). Sometimes the overhead is on the stack, sometimes not (depends on the OS and on the mutex implementation).
Here is a sample survey of sizeof() on one of my platforms I need to support:
sizeof(mutex) = 44 sizeof(try_mutex) = 44 sizeof(timed_mutex) = 76 sizeof(recursive_mutex) = 44 sizeof(recursive_try_mutex) = 44 sizeof(recursive_timed_mutex) = 80 sizeof(rw_mutex) = 108
On another platform it looks like:
sizeof(mutex) = 44 sizeof(try_mutex) = 44 sizeof(timed_mutex) = 76 sizeof(recursive_mutex) = 80 sizeof(recursive_try_mutex) = 80 sizeof(recursive_timed_mutex) = 80 sizeof(rw_mutex) = 108
On a third platform I support there are differences between mutex and try_mutex, but the difference is not well illustrated by sizeof() because the "mutex type" is just a 4 byte handle in some cases (but not others).
sizeof(mutex) = 24 sizeof(try_mutex) = 4 sizeof(timed_mutex) = 4 sizeof(recursive_mutex) = 24 sizeof(recursive_try_mutex) = 4 sizeof(recursive_timed_mutex) = 4 sizeof(rw_mutex) = not implemented
And of course all of this is subject to change. But if I'm forced to include all capability into one mutex type, then there is definitely going to be a price paid on some platforms by those who only want a mutex (which is also by far the most often needed type).
Thanks for the information. I take your point that lock is pretty much the same size regardless of the mutex. The mutex size variation does not concern me too much but I could see it will concern some, e.g. an embedded resource poor system or a design where you might go crazy and have a million mutexes which is something I'm thinking off as a space / time tradeoff. Regards, Matt Hurd.

On Jul 16, 2004, at 1:21 AM, Matt Hurd wrote:
On Thu, 15 Jul 2004 22:30:37 -0400, Howard Hinnant <hinnant@twcny.rr.com> wrote:
On Jul 15, 2004, at 7:42 PM, Matt Hurd wrote:
The constructor for transfer_lock takes two locks, referencing the same mutex, the first of which must be unlocked, and the second locked.
In principle, tranferring the true from one bool to another sounds a nice thing to do, however:
I'm not sure this is a good idea. ... Do you have a specific use case in mind?
Yes, the upgradable_read_lock discussed in this thread. Sometimes you need to transfer ownership from an upgradable_read_lock to a write_lock, or vice-versa. Note, this is not transferring ownership among threads, but within one thread. The transfer_lock utility can help you do the transfer with the RAII idiom (do the transfer in transfer_lock's constructor, and reverse the transfer back in the destructor).
The constructor for lock_both takes two locks, referencing different mutexes, both of which must be unlocked.
Example use:
mutex m; scoped_lock lock1(m1, deferred); // or whatever syntax for not locking scoped_lock lock2(m2, deferred); lock_both<scoped_lock, scoped_lock> lock(lock1, lock2); // m1 and m2 atomically locked here, without fear of deadlock
Without the ability to construct but defer locking of lock1 and lock2, the construction of lock_both becomes inefficient and awkward.
My immediate reaction was to think this was a bad idea, but a moment later I'm not so sure.
My initial thought, and I think it is still valid, was that locking many things atomically doesn't make sense. You have to lock them one after the other and always in the same order to prevent deadlock. Perhaps you have a scheme in mind where you try lock for all things and release if you can't get all and then try again causing a group spin lock? I think I'm missing a part of your picture, let me know what you're thinking.
Consider: class A { typedef rw_mutex mutex; typedef mutex::read_lock read_lock; typedef mutex::write_lock write_lock; typedef lock_both<write_lock, read_lock> lock_both; public: ... A& operator = (const A& a); ... private: mutable mutex mut_; int i_; // just example data int j_; // just example data }; A& A::operator = (const A& a) { if (this != &a) { write_lock wl(mut_, false); read_lock rl(a.mut_, false); lock_both lk(wl, rl); i_ = a.i_; j_ = a.j_; } return *this; } The operator= must make sure the rhs is safe for reading, and the lhs is safe for writing. Since each object is protected with its own mutex, two mutexes must be locked. It is convenient to have lock_both take care of the details of how to atomically lock these two mutexes without deadlock, whether that is by ordering, or by try-and-back-off. As discussed earlier in this thread, the try-and-back-off algorithm will not lead to spin, and indeed may offer advantages over ordering. Whatever the algorithm, it is good to encapsulate it.
Thanks for the information. I take your point that lock is pretty much the same size regardless of the mutex. The mutex size variation does not concern me too much but I could see it will concern some, e.g. an embedded resource poor system or a design where you might go crazy and have a million mutexes which is something I'm thinking off as a space / time tradeoff.
Mutex size concerns me because it is common to put a mutex into an object (like A above). And one can easily have many A's (e.g. vector<A>). Locks otoh are usually locally declared stack-based objects. -Howard

Eric Niebler wrote:
Peter Dimov wrote:
The "unified" timed lock case is
{ scoped_lock l( m, false );
if( l.timed_lock( t ) ) { // do stuff } else { // what do we do here? presumably either:
// setup for unbounded wait l.lock(); // do stuff
// or signal failure } }
The scoped_timed_lock use case also needs to eventually contain an if( l ) statement; you can't do much if you don't know whether your timed lock succeeded. One might also argue that a separate scoped_timed_lock makes accidentally omitting the if() statement more likely and harder to spot.
But it's hard to tell without real use cases.
I continue to believe that a better interface for a unified lock involves descriptively named helper functions:
I like it better too, but...
scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock
It's just as clear as you could hope for, and it's extensible. I feel that the single lock c'tor with a bool arg is less readable and rather inflexible.
Earlier I posted code demonstrating how this interface could be made to work.
I believe there was (nearly?) consensus that we want to make locks movable at some point. My conclusion was that at that point this syntax becomes possible, too. I think Peter said or implied much the same thing, maybe before I did. As I mentioned in another recent reply, the unified lock won't be in the upcoming release; making locks movable may happen before the next release after that, making this syntax possible in that release also. In your opinion, should a unified lock then have no locking constructors, or should it have only the one already proposed? Mike

Michael Glassford wrote:
Eric Niebler wrote:
I continue to believe that a better interface for a unified lock involves descriptively named helper functions:
I like it better too, but...
scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock
It's just as clear as you could hope for, and it's extensible. I feel that the single lock c'tor with a bool arg is less readable and rather inflexible.
Earlier I posted code demonstrating how this interface could be made to work.
I believe there was (nearly?) consensus that we want to make locks movable at some point. My conclusion was that at that point this syntax becomes possible, too. I think Peter said or implied much the same thing, maybe before I did.
As I mentioned in another recent reply, the unified lock won't be in the upcoming release; making locks movable may happen before the next release after that, making this syntax possible in that release also.
This is true, but once we add a scoped_lock constructor that takes a bool, we can't take it back out. If we then added helper functions, we'd be giving people two ways to do the same thing. Not a big deal I suppose, but I'm a minimalist and would prefer only one way.
In your opinion, should a unified lock then have no locking constructors, or should it have only the one already proposed?
My opinion? Hmm ... I'd like to see scoped_lock have one public c'tor that took a mutex and locked unconditionally. That's the most common case, so it should be simple and concise. Everything else would be an appropriately named helper function. My $0.000002. -- Eric Niebler Boost Consulting www.boost-consulting.com

Michael Glassford wrote:
I believe there was (nearly?) consensus that we want to make locks movable at some point. My conclusion was that at that point this syntax becomes possible, too. I think Peter said or implied much the same thing, maybe before I did.
I'm just going to be the voice of dissent here, again, and argue that the scoped_locks, at least, should not be moveable, otherwise they're not very scoped. The scoped lock serves a specific purpose in ensuring that the mutex doesn't remain locked outside of block scope, just like scoped_ptr ensures that a heap object has a specific lifetime. If there's enough hew and cry for a movable lock, then we should create a new class for it. That said, most of the arguments I could find on the list archive for it are to support syntax changes in initializing a scoped lock. I also saw an argument for storing locks in containers, which I can't quite fathom the need for, but I welcome the use cases. As a compromise, we could create lock_transfer<> objects that wouldn't lock a mutex, but would allow initialization syntax in places where the language makes it difficult (this is just a quick sketch, there may be errors): template <typename Mutex> class lock_transfer; template <typename Mutex> class try_lock_transfer; template <typename Mutex> lock_transfer<Mutex> lock( Mutex & m ); template <typename Mutex> try_lock_transfer<Mutex> try_lock( Mutex & m ); template <typename Mutex> class lock_transfer { private: friend class scoped_lock<Mutex>; friend class scoped_try_lock<Mutex>; friend lock_transfer<Mutex> lock<>( Mutex & m ); Mutex & m_; explicit lock_tranfer( Mutex & m ) : m_( m ) { } }; template <typename Mutex> lock_transfer<Mutex> lock( Mutex & m ) { return lock_transfer<Mutex>( m ); } template <typename Mutex> class try_lock_transfer { private: friend class scoped_try_lock<Mutex>; friend try_lock_transfer<Mutex> try_lock<>( Mutex & m ); Mutex & m_; explicit try_lock_tranfer( Mutex & m ) : m_( m ) { } }; template <typename Mutex> try_lock_transfer<Mutex> try_lock( Mutex & m ) { return try_lock_transfer<Mutex>( m ); } template <typename Mutex> class scoped_lock { Mutex & m_; public: explicit scoped_lock( Mutex & m ) : m_( m ) { lock(); } // blocking lock transfer explicit scoped_lock( lock_transfer<Mutex> const & t ) : m_( t.m_ ) { lock(); } scoped_lock & operator=( lock_tranfer<Mutex> const & t ) : m_( t.m_ ) { lock(); return *this; } operator /* convertible to bool */() const { return locked(); } }; template <typename Mutex> class scoped_try_lock { Mutex & m_; public: explicit scoped_try_lock( Mutex & m ) : m_( m ) { try_lock(); } // blocking lock transfer explicit scoped_lock( lock_transfer<Mutex> const & t ) : m_( t.m_ ) { lock(); } scoped_lock & operator=( lock_tranfer<Mutex> const & t ) : m_( t.m_ ) { lock(); return *this; } // try lock transfer explicit scoped_lock( try_lock_transfer<Mutex> const & t ) : m_( t.m_ ) { try_lock(); } scoped_lock & operator=( try_lock_tranfer<Mutex> const & t ) : m_( t.m_ ) { try_lock(); return *this; } operator /* convertible to bool */() const { return locked(); } }; void foo() { mutex m; try_mutex try_m; if ( mutex::scoped_lock l = lock( m ) ) // always true { // do stuff } if ( mutex::scoped_lock l = try_lock( m ) ) // compile error! { // never reached } if ( try_mutex::scoped_try_lock l = lock( m ) ) // always true { // do stuff } if ( try_mutex::scoped_try_lock l = try_lock( m ) ) // may be false { // do stuff } } Comments and critique welcome, Christopher -- Christopher Currie <codemonkey@gmail.com>

Christopher Currie wrote:
Michael Glassford wrote:
I believe there was (nearly?) consensus that we want to make locks movable at some point. My conclusion was that at that point this syntax becomes possible, too. I think Peter said or implied much the same thing, maybe before I did.
I'm just going to be the voice of dissent here, again, and argue that the scoped_locks, at least, should not be moveable, otherwise they're not very scoped. The scoped lock serves a specific purpose in ensuring that the mutex doesn't remain locked outside of block scope, just like scoped_ptr ensures that a heap object has a specific lifetime.
If there's enough hew and cry for a movable lock, then we should create a new class for it. That said, most of the arguments I could find on the list archive for it are to support syntax changes in initializing a scoped lock.
Did you see http://aspn.activestate.com/ASPN/Mail/Message/boost/1207226, and especially http://aspn.activestate.com/ASPN/Mail/Message/1209126?
I also saw an argument for storing locks in containers, which I can't quite fathom the need for, but I welcome the use cases.
As a compromise, we could create lock_transfer<> objects that wouldn't lock a mutex, but would allow initialization syntax in places where the language makes it difficult (this is just a quick sketch, there may be errors):
[snip code example] Mike

Michael Glassford wrote:
If there's enough hew and cry for a movable lock, then we should create a new class for it. That said, most of the arguments I could find on the list archive for it are to support syntax changes in initializing a scoped lock.
Did you see http://aspn.activestate.com/ASPN/Mail/Message/boost/1207226, and especially http://aspn.activestate.com/ASPN/Mail/Message/1209126?
No I hadn't (kudos to you for digging this up!). It's good stuff, similar in spirit to my post, the major difference is that Bill's suggestion attempts to implement real move semantics, whereas my sketch was simply an initialization convenience. I'll spend some time looking at the thread in more detail later when I have time, but my initial impression is that although the ability to create a lock and return it out of a function may make certain techniques easier, I don't think the lack of it makes them impossible. Specifically, I have a sketch in my head of locking_ptr<> that wouldn't require move semantics, but I'd have to see what code Bill had in mind. I reread the Stroustrop article, which I'd seen before, and I can imagine how his Wrap<> could be used with our existing locks and boost::ref. Christopher -- Christopher Currie <codemonkey@gmail.com>

On Jul 16, 2004, at 1:41 PM, Christopher Currie wrote:
I'm just going to be the voice of dissent here, again, and argue that the scoped_locks, at least, should not be moveable, otherwise they're not very scoped. The scoped lock serves a specific purpose in ensuring that the mutex doesn't remain locked outside of block scope, just like scoped_ptr ensures that a heap object has a specific lifetime.
The first practical application of move semantics to locks that I saw was when I started playing around with read/write/upgradable locks. The very idea of an upgradable_read_lock is that it can transfer ownership to a write_lock. And if you cast this operation in the move syntax suggested by n1377 you can get some fairly elegant code (imho). Consider a class A with a rw_mutex as a member: class A { typedef Metrowerks::rw_mutex mutex; typedef mutex::read_lock read_lock; typedef mutex::write_lock write_lock; typedef mutex::upgradable_read_lock upgradable_read_lock; public: ... void read_write_one_way(); void read_write_another_way(); ... private: mutable mutex mut_; }; There are two public functions (among others), that both read and write. The rw_mutex must be locked accordingly. For convenience, it would be nice if they could implement themselves in terms of a common private read_impl(). Something like: void A::read_write_one_way() { read_impl(); write_lock wl(mut_); // do the write } This doesn't quite work because you don't want to free the read_lock under read_impl() until you've obtained the write_lock in read_write_one_way. You could: class A { typedef Metrowerks::rw_mutex mutex; ... public: ... void read_write_one_way(); void read_write_another_way(); ... private: mutable mutex mut_; void read_impl() const; }; void A::read_impl() const { // precondition: client has at least read-locked // do the read } void A::read_write_one_way() { upgradable_read_lock url(mut_); read_impl(); write_lock wl(url); // transfer ownership // do the write } void A::read_write_another_way() { upgradable_read_lock url(mut_); read_impl(); write_lock wl(url); // transfer ownership // do the write } But if we really had move semantics as in n1377, I'd be tempted to: class A { typedef Metrowerks::rw_mutex mutex; ... public: ... void read_write_one_way(); void read_write_another_way(); ... private: mutable mutex mut_; upgradable_read_lock read_impl() const; }; A::upgradable_read_lock A::read_impl() const { // process some stuff before needing to read-lock upgradable_read_lock url(mut_); // do the read return url; } void A::read_write_one_way() { write_lock wl(read_impl()); // transfer ownership // do the write } void A::read_write_another_way() { write_lock wl(read_impl()); // transfer ownership // do the write } The functionality is the same with either coding, but in the latter I can put the read-locking logic where it belongs (in read_impl) instead of depending upon each read_write function to correctly do the read-locking. It also allows the possibility that read_impl() may not need to read-lock right at the top of its function, but perhaps part way through, resulting in higher concurrency (the less locked the better). <shrug> I don't see this as a huge win. But it does seem like a reasonable application for moving locks. If you were to tell me this scenario would be pretty rare, I'd have to agree with you. I imagine 90% of all lock use would be a simple scoped_lock (no try, no timed, no recursive, no read/write). -Howard

Christopher Currie wrote:
As a compromise, we could create lock_transfer<> objects that wouldn't lock a mutex, but would allow initialization syntax in places where the language makes it difficult (this is just a quick sketch, there may be errors):
<code snipped> This won't work. Try to compile this with Comeau online: struct lock_transfer {}; struct scoped_lock { scoped_lock( lock_transfer ) {} private: scoped_lock( scoped_lock const & ); scoped_lock & operator =( scoped_lock const & ); }; int main() { scoped_lock l = lock_transfer(); return 0; } You'll get: "ComeauTest.c", line 13: error: "scoped_lock::scoped_lock(const scoped_lock &)", required for copy that was eliminated, is inaccessible scoped_lock l = lock_transfer(); ^ Non-copyable types can't be initialized this way. The code I posted earlier worked around this problem by enabling just enough move semantics to allow a lock to be returned from a function, but nothing else. The lock is still non-copyable. -- Eric Niebler Boost Consulting www.boost-consulting.com

Eric Niebler wrote:
This won't work. Try to compile this with Comeau online:
You'll get:
"ComeauTest.c", line 13: error: "scoped_lock::scoped_lock(const scoped_lock &)", required for copy that was eliminated, is inaccessible scoped_lock l = lock_transfer(); ^
Non-copyable types can't be initialized this way. The code I posted earlier worked around this problem by enabling just enough move semantics to allow a lock to be returned from a function, but nothing else. The lock is still non-copyable.
*pouts* Well, fooey. Glad you got it working, though, I missed that post. My point still stands, I think the movable lock should be a separate type from the scoped lock, even if we implement minimal move semantics to support assignment initialziation. Christopher -- Christopher Currie <codemonkey@gmail.com>

Eric Niebler wrote:
I continue to believe that a better interface for a unified lock involves descriptively named helper functions:
scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock
Maybe. Do you have a sketch of the specification? (Not the implementation.)

Peter Dimov wrote:
Eric Niebler wrote:
I continue to believe that a better interface for a unified lock involves descriptively named helper functions:
scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock
Maybe. Do you have a sketch of the specification? (Not the implementation.)
I know it's not particulary strong point, but I used approach propsed by Eric in the past, and it worked pretty well. I used something similar to: template <typename T> inline typename lock::scoped_lock_impl<T> acquire(lock::lock_impl<T>& lock) { return lock::scoped_lock_impl<T>(lock); } template <typename T> inline typename lock::scoped_lock_impl<T> try_acquire(lock::lock_impl<T>& lock, unsigned long timeout) { return lock::scoped_lock_impl<T>(lock, timeout); } template <typename T> inline typename lock::scoped_lock_impl<T> try_acquire(lock::lock_impl<T>& lock) { return lock::scoped_lock_impl<T>(lock, lock::scoped_lock_impl<T>::dont_wait_t()); } Where scoped_lock_impl was similar to: struct scoped_lock_t { protected: scoped_lock_t(); ~scoped_lock_t(); // safe_bool is typedef to "pointer to member of private struct" // ... public: virtual bool active() const = 0; operator safe_bool() const { return active(); } }; typedef scoped_lock_t& scoped_lock; template <typename T> class scoped_lock_impl : public scoped_lock_t { lock_impl<T>* lock_; // non-assignable scoped_lock_impl<T>& operator= (scoped_lock_impl<T>& other); public: struct dont_wait_t {}; virtual bool active() const { return lock_ != NULL; } explicit scoped_lock_impl(lock_impl<T>& lock) : lock_(&lock) { lock.acquire(); } scoped_lock_impl(lock_impl<T>& lock, unsigned long timeout) : lock_(lock.try_acquire(timeout)) {} scoped_lock_impl(lock_impl<T>& lock, const dont_wait_t&) : lock_(lock.try_acquire()) {} lock_impl<T>* release() { lock_impl<T>* tmp = lock_; lock_ = NULL; return tmp; } // cctor is passing ownership scoped_lock_impl(scoped_lock_impl& other) : lock_(other.release()) {} ~scoped_lock_impl() { if (lock_) lock_->release(); } }; and lock_impl is: template <typename T> class lock_impl { protected: lock_impl() {} ~lock_impl() {} void acquire() { (static_cast<T*> (this))->acquire(); } T* try_acquire() { return (static_cast<T*> (this))->try_acquire(); } T* try_acquire(unsigned long timeout) { return (static_cast<T*> (this))->try_acquire(timeout); } void release() { (static_cast<T*> (this))->release(); } }; and actual syncronization primitives are: class critical_section : public lock::lock_impl<critical_section> { // ... }; class mutex : public lock::lock_impl<mutex> { // ... }; Here is simple usage: { scoped_lock l1 = acquire(mymutex1_); // ok, we own mutex now } // now we do not own it { scoped_lock l2 = try_acquire(mymutex2_); if (l.active()) { // we own mutex } // we still own mutex here } or if (scoped_lock l3 = try_acquire(mymutex2_, 1000L)) { // we own mutex } If type of mymutex_ changes from mutex to critical_section, creation of l3 will no longer compile (crit. section does not support timed lock), but no other code changes are required. And compiler error will clearly point to the problem. Creation of l2 will still compile, as try_acquire with no timeout is using different constructor of scoped_lock_impl. B.

Peter Dimov wrote:
Eric Niebler wrote:
I continue to believe that a better interface for a unified lock involves descriptively named helper functions:
scoped_lock l1( m ); // lock unconditionally scoped_lock l1 = defer_lock( m ); // don't lock scoped_lock l2 = try_lock( m ); // try lock scoped_lock l3 = timed_lock( m, t ); // timed lock
Maybe. Do you have a sketch of the specification? (Not the implementation.)
I don't, and I have family in town until Wednesday. (Arg! Lame answer, I know.) I just looked for my post in the ASPN archive, and it seems to have vanished into the ether, so I'm reproducing the code here. struct lock; lock try_lock(); struct lock_ref { lock_ref(lock & ref) : ref_(ref) { } lock & ref_; }; struct lock { lock(lock_ref ref) { // ... move the lock from ref.ref_ ... } operator lock_ref() { 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.) -- Eric Niebler Boost Consulting www.boost-consulting.com

Eric Niebler wrote:
(Some fancy auto_ptr-like move shenanigans are needed to make the lock return-able but not copy-able.)
I think there's a better way to express types that are movable but not copyable, which could work on more compilers. struct lock; lock try_lock(); template<class T, class U> struct enable_move {}; template<class T> struct enable_move<T, T const> { typedef typename T::error_cant_move_from_const type; }; template<class T> struct enable_move<T, T> { typedef int type; }; struct lock { lock(lock const& other) { // move the lock from const_cast<lock&>(other) } lock& operator=(lock const& other) { // move the lock from const_cast<lock&>(other) } operator bool() const { // TODO return true iff we're holding the lock return true; } private: friend lock try_lock(); lock(){} // a try-lock c'tor // make this type non-copyable, non-assignable template<class T> lock(T&, typename enable_move<lock, T>::type = 0); template<class T> typename enable_move<lock, T>::type operator=(T&); }; inline lock try_lock() { // call a special try-lock c'tor on lock return lock(); } int main() { if( lock l = try_lock() ) { } if ( lock const& l = try_lock() ) // doesn't compile on comeau with // the auto_ptr ref thing { } } -- Daniel Wallin

Daniel Wallin <dalwan01@student.umu.se> writes:
Eric Niebler wrote:
(Some fancy auto_ptr-like move shenanigans are needed to make the lock return-able but not copy-able.)
I think there's a better way to express types that are movable but not copyable, which could work on more compilers.
This looks a lot like the move library idiom that's in the sandbox. Interesting the way you prevent copying, though -- maybe that'll work on vc6. Since vc6 binds temporaries to non-const references willy-nilly I found I couldn't prevent copying. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

David Abrahams wrote:
Daniel Wallin <dalwan01@student.umu.se> writes:
I think there's a better way to express types that are movable but not copyable, which could work on more compilers.
This looks a lot like the move library idiom that's in the sandbox. Interesting the way you prevent copying, though -- maybe that'll work on vc6. Since vc6 binds temporaries to non-const references willy-nilly I found I couldn't prevent copying.
Yes, I think it should work on VC6. However, since SFINAE won't work, it will break is_convertible (because the template constructor will be enabled for anything). -- Daniel Wallin

I have attempted a specification of moving scoped_lock/read_lock/upgradable_read_lock/write_lock at: http://home.twcny.rr.com/hinnant/cpp_extensions/threads.html This is an attempt to look forward to locks+move semantics, taking into account the recent discussions. Because of the length of the specification I put it in an html on my personal website instead of stuffing it into an email. Comments welcome. Any response from me is likely to be delayed by several days due to travel. -Howard

Howard Hinnant wrote:
http://home.twcny.rr.com/hinnant/cpp_extensions/threads.html
I have number of comments: * do we need distinction between "write_lock" and "scoped_lock"? After all, both lock are: 1. exclusive (in regard to ownership of "mutex" object) 2. moveable (ie. have move semantics instead of copy semantics) write_lock delivers some superset of functionality that scoped_lock does not have, but do we need separate class for this functionality? After all "if the client of scoped_lock<Mutex> does not use funtionality which the Mutex does not supply, no harm is done". I understand that under current proposal mutex class may deliver three separate families of lock functions : lock, read_lock and write_lock, but I want you to consider just two: lock (== exclusive == write) and shared_lock (== read). I suggest that scoped_lock (another possible name "exclusive_lock") would be move-constructible also from upgradable_shared_lock. If Mutex class does not deliver member function unlock_upgradable_shared_lock_exclusive , we would have compilation error. Otherwise, no harm is done. The same applies to move-assignment from upgradable_shared_lock (hm, some shorter name please?) * I do not quite like "write_lock(try_rvalue_ref<upgradable_read_lock<mutex_type> > r);" (neither "exclusive_lock(try_rvalue_ref<upgradable_shared_lock<mutex_type> > r)" ). It seems inconsistent with rest of interface. What about "write_lock (rvalue_ref<upgradable_read_lock<mutex_type> > r, blocking_enum block_it);" ? * mutex() memeber function may return void* instead of "const mutex_type*". This will make it suitable just to check for equality of owned mutex, while preventing any kind of mutex manipulation. Just an idea, because "const" (which is already there) in most cases should suffice. * I'd still like to have number of free functions: template <typename Mutex> scoped_lock<Mutex> lock_mutex(Mutex); // or "acquire" template <typename Mutex> scoped_lock<Mutex> try_lock_mutex(Mutex); // nonblocking; or "try_acquire" template <typename Mutex> scoped_lock<Mutex> try_lock_mutex(Mutex, const elapsed_time&); // or "try_acquire" These functions might look little different (eg. use enumerations locking_enum and blocking_enum). The idea is that function will create lock appropriate for given Mutex type - assuming that there migh be Mutex-es delivered with their own lock class, or that. * I'd like to have common base class over all lock classes. I do not want polymorphic behaviour (all member functions might be protected, including destructor, ctor and cctor), but to allow following very simple and I believe typical usage scenarios: (following typedef defined in library) typedef common_lock_base& lock; // scenario 1. { lock l1 = lock_mutex(SomeMutex1); } // scenario 2. if (lock l2 = try_lock_mutex(SomeMutex2, non_blocking)) { // great, we acquired SomeMutex2 } In both scenarios we do not know what's actual type of SomeMutexX, and functions will create appropriate lock type for type of SomeMutexX (this is how I used my own very simple threading library). Both points (free functions and common base class) are just ideas - possibly we could live without it. My idea of threads interface was that lock classes have much simpler interface, while we would have number of free functions used to create them. Given amount of lock constructors in proposed interface these free functions are just an addition. About first point (IMHO unnecessary distinction between scoped_lock and write_lock) - obviously I could be wrong, but at least I'd like to hear that :) B. PS. I allowed myself to ask Kevlin Henney to evaluate this design; on recent ACCU conference he presented number of ideas about threading interfaces, see http://www.accu.org/conference/presentations/Henney_-_More_C++_Threading.pdf

Howard Hinnant wrote:
I have attempted a specification of moving scoped_lock/read_lock/upgradable_read_lock/write_lock at:
http://home.twcny.rr.com/hinnant/cpp_extensions/threads.html
This is an attempt to look forward to locks+move semantics, taking into account the recent discussions. Because of the length of the specification I put it in an html on my personal website instead of stuffing it into an email. Comments welcome. Any response from me is likely to be delayed by several days due to travel.
I mostly agree with Bronek Kozicki. Given a movable lock, Eric Niebler's proposal: scoped_lock try_lock( Mutex & m ); scoped_lock timed_lock( Mutex & m ); is a better try/timed interface. Heisenberg constructors must die. Also, I still think that the bool parameter is better than a "locking" enum, as evidenced by the use case shown in the specification: scoped_lock lock( m, want_to_lock? locking: non_locking ); I find this an unnecessarily verbose and contrived way to express scoped_lock lock( m, want_to_lock ); which is the original intent. I disagree that void * mutex() const; is a better mutex() accessor. On the contrary, I'd argue that mutex() should return a non-const mutex_type. A const return doesn't make a lot of sense given that our mutexes have no const member functions; the private interface is a better protection than the const. On the other hand, a non-const return allows the client to use scoped_lock<> on user-defined mutex types (in implementing the user-defined condition::wait, for example). I also like the proposed scoped_lock == write_lock unification.

Peter Dimov wrote:
I mostly agree with Bronek Kozicki.
Thank you :)
I disagree that
void * mutex() const;
is a better mutex() accessor. On the contrary, I'd argue that mutex() should return a non-const mutex_type. A const return doesn't make a lot of sense given that our mutexes have no const member functions; the private interface is a better protection than the const.
good point.
On the other hand, a non-const return allows the client to use scoped_lock<> on user-defined mutex types (in implementing the user-defined condition::wait, for example).
another good point. But then, I think that mutex() should be non-const in order to make it clear that owned mutex might be modified, and we should have another const member function just for one purpose - test if two locks are for the same mutex: template <typename Lock> bool same_mutex(const Lock&) const; or maybe just: bool same_mutex(const base_lock_class& ) const; or maybe (???) overloaded operator== Incidentally I think that function same_mutex could be considered as one of two (besides "bool locked() const;") publicly available members of base_lock_class. I'm not sure that it's good idea, but maybe it's worth consideration. Locks do not have to be polymorphic (as in OOP) in order to provide this functionality, but still it will bring some extra cost. B.

From: "Peter Dimov" <pdimov@mmltd.net>
Also, I still think that the bool parameter is better than a "locking" enum, as evidenced by the use case shown in the specification:
scoped_lock lock( m, want_to_lock? locking: non_locking );
I find this an unnecessarily verbose and contrived way to express
scoped_lock lock( m, want_to_lock );
which is the original intent.
I disagree. From the perspective of calling code, using a bool makes things more confusing: scoped_lock lock1(m, true); scoped_lock lock2(m, false);
From this context, it isn't apparent what "true" and "false" mean. Yes, you can refer to the documentation, but what if you're new to scoped_lock and you forget what it means? Then you'll have to switch contexts, find the docuementation, read about the parameter, and then switch back to see what's really happening in the code.
With the enum, however, the code is self-documenting: scoped_lock lock1(m, locking); scoped_lock lock2(m, non_locking); Thus, using the enum eliminates a source of errors and confusion whereas the conditional behavior you mention is still possible, if verbose. Another way to eliminate the source of errors is to provide helper functions as the only way to create the locks: scoped_lock lock1(make_locking_scoped_lock(m)); scoped_lock lock1(make_non_locking_scoped_lock(m)); That makes your example, and all scoped_lock code, uglier, though: scoped_lock lock(want_to_lock ? make_locking_scoped_lock(m) : make_non_locking_scoped_lock(m)); -- Rob Stewart stewart@sig.com Software Engineer http://www.sig.com Susquehanna International Group, LLP using std::disclaimer;

Rob Stewart wrote:
From: "Peter Dimov" <pdimov@mmltd.net>
Also, I still think that the bool parameter is better than a "locking" enum, as evidenced by the use case shown in the specification:
scoped_lock lock( m, want_to_lock? locking: non_locking );
I find this an unnecessarily verbose and contrived way to express
scoped_lock lock( m, want_to_lock );
which is the original intent.
I disagree. From the perspective of calling code, using a bool makes things more confusing:
scoped_lock lock1(m, true); scoped_lock lock2(m, false);
From this context, it isn't apparent what "true" and "false" mean. Yes, you can refer to the documentation, but what if you're new to scoped_lock and you forget what it means? Then you'll have to switch contexts, find the docuementation, read about the parameter, and then switch back to see what's really happening in the code.
With the enum, however, the code is self-documenting:
scoped_lock lock1(m, locking); scoped_lock lock2(m, non_locking);
Thus, using the enum eliminates a source of errors and confusion whereas the conditional behavior you mention is still possible, if verbose.
With a movable lock, you could do this: scoped_lock lock_if( Mutex & m, bool lock_it ) { return scoped_lock( m, lock_it ? locking : non_locking ); } And now the point of use looks like: scoped_lock l = lock_if( m, my_condition ); As I've mentioned previously, I prefer to drop the enum entirely and use a helper function to defer taking the lock: scope_lock l = defer_lock( m ); Given such a scheme, lock_if as defined above would be implemented as: scoped_lock lock_if( Mutex & m, bool lock_it ) { scoped_lock l = defer_lock( m ); if( lock_it ) l.lock(); return m; } Movable locks open up all sorts of interesting interface possibilities. -- Eric Niebler Boost Consulting www.boost-consulting.com

Rob Stewart wrote:
From: "Peter Dimov" <pdimov@mmltd.net>
Also, I still think that the bool parameter is better than a "locking" enum, as evidenced by the use case shown in the specification:
scoped_lock lock( m, want_to_lock? locking: non_locking );
I find this an unnecessarily verbose and contrived way to express
scoped_lock lock( m, want_to_lock );
which is the original intent.
I disagree. From the perspective of calling code, using a bool makes things more confusing:
scoped_lock lock1(m, true); scoped_lock lock2(m, false);
From this context, it isn't apparent what "true" and "false" mean. Yes, you can refer to the documentation, but what if you're new to scoped_lock and you forget what it means? Then you'll have to switch contexts, find the docuementation, read about the parameter, and then switch back to see what's really happening in the code.
With the enum, however, the code is self-documenting:
scoped_lock lock1(m, locking); scoped_lock lock2(m, non_locking);
Thus, using the enum eliminates a source of errors and confusion whereas the conditional behavior you mention is still possible, if verbose.
You could have written that about any interface that takes a bool. Perhaps if you take into account some specific properties of scoped_lock you'll be able to argue more convincingly. For example: - a 'locking' lock is actually spelled scoped_lock lock1( m ); - the conditional behavior was actually in Howard's specification as the use case for the constructor, it wasn't merely mentioned by me; - the non-locking case: scoped_lock lock2( m, false ); scoped_lock lock2( m, non_locking ); usually crops up when lock2 is about to be try_locked or timed_locked immediately thereafter. If we include scoped_lock try_mutex_lock( Mutex & m ); that enable Eric Niebler's if( scoped_lock lock2 = try_mutex_lock( m ) ) this will reduce the frequency of the "non-locking locks" considerably.

From: "Peter Dimov" <pdimov@mmltd.net>
Rob Stewart wrote:
From: "Peter Dimov" <pdimov@mmltd.net>
Also, I still think that the bool parameter is better than a "locking" enum, as evidenced by the use case shown in the specification:
scoped_lock lock( m, want_to_lock? locking: non_locking );
I find this an unnecessarily verbose and contrived way to express
scoped_lock lock( m, want_to_lock );
which is the original intent.
I disagree. From the perspective of calling code, using a bool makes things more confusing: [snip] From this context, it isn't apparent what "true" and "false" mean. Yes, you can refer to the documentation, but what if [snip] With the enum, however, the code is self-documenting:
scoped_lock lock1(m, locking); scoped_lock lock2(m, non_locking);
Thus, using the enum eliminates a source of errors and confusion whereas the conditional behavior you mention is still possible, if verbose.
You could have written that about any interface that takes a bool. Perhaps
Exactly.
if you take into account some specific properties of scoped_lock you'll be able to argue more convincingly. For example:
- a 'locking' lock is actually spelled
scoped_lock lock1( m );
Then why wasn't the example written like this? scoped_lock lock(want_to_lock ? scoped_lock(m) : scoped_lock(m, non_locking)); That was a rhetorical question (IIRC, and I'm not following this discussion closely, scoped_locks are not copyable, so you can't write this version).
- the conditional behavior was actually in Howard's specification as the use case for the constructor, it wasn't merely mentioned by me;
OK, but you used it as the basis for justifying using bool rather than the enum. I'll grant you that I didn't have the whole picture to which you were apparently alluding; I just took your statements at face value.
- the non-locking case:
scoped_lock lock2( m, false ); scoped_lock lock2( m, non_locking );
usually crops up when lock2 is about to be try_locked or timed_locked immediately thereafter. If we include
scoped_lock try_mutex_lock( Mutex & m );
that enable Eric Niebler's
if( scoped_lock lock2 = try_mutex_lock( m ) )
this will reduce the frequency of the "non-locking locks" considerably.
Doesn't that argue for the case of making the "key" that indicates that the lock should be "non-locking" more explicit than a bool? IOW, if one is rarely going to write "scoped_lock(m, false)," then the meaning of "false" is even more obscure and "non_locking" is even more appropriate. -- Rob Stewart stewart@sig.com Software Engineer http://www.sig.com Susquehanna International Group, LLP using std::disclaimer;

Rob Stewart <stewart@sig.com> writes:
Doesn't that argue for the case of making the "key" that indicates that the lock should be "non-locking" more explicit than a bool? IOW, if one is rarely going to write "scoped_lock(m, false)," then the meaning of "false" is even more obscure and "non_locking" is even more appropriate.
"non_locking" is a terribly obscure name to associate with a lock. I really think "deferred" works well. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

From: David Abrahams <dave@boost-consulting.com>
Rob Stewart <stewart@sig.com> writes:
Doesn't that argue for the case of making the "key" that indicates that the lock should be "non-locking" more explicit than a bool? IOW, if one is rarely going to write "scoped_lock(m, false)," then the meaning of "false" is even more obscure and "non_locking" is even more appropriate.
"non_locking" is a terribly obscure name to associate with a lock. I really think "deferred" works well.
I can't disagree. I was just using the name already being proffered. -- Rob Stewart stewart@sig.com Software Engineer http://www.sig.com Susquehanna International Group, LLP using std::disclaimer;

"Peter Dimov" <pdimov@mmltd.net> writes:
I mostly agree with Bronek Kozicki. Given a movable lock, Eric Niebler's proposal:
scoped_lock try_lock( Mutex & m ); scoped_lock timed_lock( Mutex & m );
is a better try/timed interface. Heisenberg constructors must die.
I'm not sure if there are any advantages, but try_lock and timed_lock could actually be derived classes of scoped_lock in that case, using an intentionally slicing move. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com

Peter Dimov wrote:
The "unified" timed lock case is
{ scoped_lock l( m, false );
if( l.timed_lock( t ) ) { // do stuff } else { // what do we do here? presumably either:
// setup for unbounded wait l.lock(); // do stuff
// or signal failure } }
I understand this is just an example, but the question I find myself asking is: if a blocking lock is acceptable, why not do it in the first place? However, let's assume that for some performance reason we'd like to see if we can get the lock quickly, and if not, then do the (potentially expensive) setup we need to allow for a blocking lock. The timed_lock class supports the above scenario as-is. And even if it didn't, the behavior could still be done: bool done = false; { scoped_timed_lock l( m, t ); if ( l.locked() ) { // do stuff done = true; } } if ( ! done ) { // setup scoped_lock l( m ); // do stuff } Granted, this may seem a bit more cumbersome to the user, which is why the scoped_timed_lock class has a blocking lock() operation built in.
The scoped_timed_lock use case also needs to eventually contain an if( l ) statement; you can't do much if you don't know whether your timed lock succeeded.
Agreed.
One might also argue that a separate scoped_timed_lock makes accidentally omitting the if() statement more likely and harder to spot.
I can see that, although one could also argue that the name of the scoped_timed_lock class is an immediate signal that one needs to check the status of the lock. It does feel like a matter of preference, though; I prefer to use the locking constructors as opposed to the non-locking constructors. Fortunately, the scoped_timed_lock supports either usage as-is, without modification.
But it's hard to tell without real use cases.
Agreed. -- Christopher Currie <codemonkey@gmail.com>

Michael Glassford wrote:
Extended lock interface ----------------------- The lock interface should be extended to make it possible to determine if it is locking a particular mutex. How?
Suggestions: Mutex& mutex() Mutex* mutex() void* mutex() mutex_id_type mutex()
Conclusion: ???
Can someone post a rationale for this? Not that I see the harm in adding it, per se, but my impression is that, if the lock exists for long enough that you've lost track of what it's locking, hasn't the mutex been locked for too long? Unless you're trying to find out from a thread that doesn't own the lock, which seems to be a shift of paradigm from the original intent. From the documentation in http://www.boost.org/libs/thread/doc/lock_concept.html: "Lock objects are meant to be short lived, expected to be used at block scope only. The lock objects are not thread-safe. Lock objects must maintain state to indicate whether or not they've been locked and this state is not protected by any synchronization concepts. For this reason a lock object should never be shared between multiple threads." As I see it, scoped locks are the physical embodiment of a critical section, and the longer your critical secion is, the more potential for it to become a bottle-neck, and the less useful concurrent programming will be. -- Christopher Currie <codemonkey@gmail.com>

In an attempt to reconcile the different preferences and ideas expressed in this thread, here's a thought experiment for mutex & lock unification. What if the mutex and lock classes were policy-based, allowing users to choose the interface they want and supplying a typedef or two for the "most common" or "approved" variations? Example below (note that I haven't tried to implement multiple policies elegantly). Hopefully it could be expanded to include recursive, checked, read/write, etc. //---------- Mutex ----------// template< typename Policy1, typename Policy2, typename Policy3 > class mutex_type : public Policy1 , public Policy2 , public Policy3 { public: mutex_type(); ~mutex_type(); protected: void do_unlock(); void do_lock(cv_state& state); void do_unlock(cv_state& state); }; class blocking_mutex_policy { protected: void do_lock(); }; class try_mutex_policy { protected: bool do_trylock(); }; //Always available but may be inefficient: class timed_mutex_policy { protected: bool do_timedlock(const xtime& xt); }; //Only available with platform support: class fast_timed_mutex_policy { protected: bool do_timedlock(const xtime& xt); }; //Example typedefs: typedef mutex_type< blocking_mutex_policy, try_mutex_policy, fast_timed_mutex_policy > mutex; typedef blocking_mutex_type< blocking_mutex_policy, null_policy, null_policy > mutex; typedef try_mutex_type< try_mutex_policy, null_policy, null_policy, > mutex; typedef timed_mutex_type< timed_mutex_policy, null_policy, null_policy, > mutex; //---------- Lock ----------// template< typename Mutex, typename Policy1, typename Policy2, typename Policy3 > class lock_type : public Policy1 , public Policy2 , public Policy3 { public: lock_type(Mutex& m, bool initially_locked = true); ~lock_type(); void unlock(); bool locked(); operator const void*() const; protected: void do_lock(cv_state& state); void do_unlock(cv_state& state); }; class blocking_lock_policy { protected: void lock(); }; class try_lock_policy { protected: void try_lock(); }; class timed_lock_policy { protected: void timed_lock(); }; //Example typedefs: typedef lock_type< blocking_lock_policy, try_lock_policy, timed_lock_policy > lock; typedef lock_type< blocking_lock_policy, null_policy, null_policy > blocking_lock; typedef lock_type< try_lock_policy, null_policy, null_policy> try_lock; typedef lock_type< timed_lock_policy, null_policy, null_policy > timed_lock;

On Fri, 16 Jul 2004 13:54:32 -0400, Michael Glassford <glassfordm@hotmail.com> wrote:
In an attempt to reconcile the different preferences and ideas expressed in this thread, here's a thought experiment for mutex & lock unification. What if the mutex and lock classes were policy-based, allowing users to choose the interface they want and supplying a typedef or two for the "most common" or "approved" variations?
Example below (note that I haven't tried to implement multiple policies elegantly). Hopefully it could be expanded to include recursive, checked, read/write, etc.
<snip>
//Example typedefs:
typedef mutex_type< blocking_mutex_policy, try_mutex_policy, fast_timed_mutex_policy > mutex;
typedef blocking_mutex_type< blocking_mutex_policy, null_policy, null_policy > mutex;
typedef try_mutex_type< try_mutex_policy, null_policy, null_policy, > mutex;
typedef timed_mutex_type< timed_mutex_policy, null_policy, null_policy, > mutex;
<snip>
//Example typedefs:
typedef lock_type< blocking_lock_policy, try_lock_policy, timed_lock_policy > lock;
typedef lock_type< blocking_lock_policy, null_policy, null_policy > blocking_lock;
typedef lock_type< try_lock_policy, null_policy, null_policy> try_lock;
typedef lock_type< timed_lock_policy, null_policy, null_policy > timed_lock;
I think this is thinking along the right track though I'm not fond of your interface above, but it is a good start to the thinking. I've been reading over some of the old boost thread threads and I think I now see why Bill Kempf is now away from boost::thread. The best spin is, I think, he gave up on consensus. It it interesting the old ground we are covering, some of the thinking back then was perhaps a little better then. Some of the current ideas seem a little better too. I do like David Held's thinking, perhaps because it is similar to mine ;-), in these mails: http://aspn.activestate.com/ASPN/Mail/Message/boost/1213138 http://aspn.activestate.com/ASPN/Mail/Message/boost/1207304 Bill Kempf was concerned about providing as safe as possible semantics for locking. He saw the need for lock transfers especially for Bjarne's auto locking proxy idiom. One way of doing this was to expose lock and unlock on a lock, but he felt exposing the same on the mutex was safer yet still troubling. Bill related transfer semantics to auto_ptr and saw the shared_ptr approach as problematic. Others are of the opinion that lock and unlock is fine and catering threading newbies should not prevent them from the low level tools that makes their life fun ;-) I side with Bill, on balance, I think on this. I see public a lock and unlock as the equivalent of a threading "goto". However, I'm not so sure about his shared pointer reluctance at one level. His argument was that a shared_pointer interface requirements are a lot different to an interface for a lock with move/transfer semantics, which is true. He also argued that sharing a lock made no sense and was dangerous. This is kind of true, but we do share a mutex with a shareable mutex (the one i keep calling a shareable mutex and the world calls a read write mutex). A non-shareable mutex in effect would have a count of zero or one (true or false) and ownership transfer would be the correct interface. Careful consideration must go to thinking about such situations as : do_something( lock_object ); w.r.t. to transfer. Perhaps lock_object = do_something( lock_object ); covers the case, but I can see the danger already... There are grounds for concern over the implementation delivery of shared / transfer implementation as it should either rely on no-locking, atomic primitives or locking primitives below the normal locking to avoid the chicken and the egg of locking a count. I would like to see David Held's RIIA mechanism, mixed with Michael's policy approach to deliver a solution that could also be consistent with the resource acquisition requirements of socket and file i/o. Timer interaction, especially because or try_lock, where the timer is a resource should perhaps be covered too. The mutex taxonomy I have in mind would be: 1. null_mutex elided ops, atomic ops with interlocking 2. simple_mutex basic os mutex / critical section recursive on windows, non-recursive on posix 3. recursive_mutex recursive always 4. shareable_mutex supports exclusive and shared access reflects os primitives where available ( what does this mean with recursiveness???) 5. upgradeable_shareable_mutex adds upgradeable read lock equivalent yeah, maybe not the best names - a lock is a resource acquisition on a mutex. - avoid lock and unlock methods if possible via move/transfer semantics - consider a read lock as a shared_lock, lots of owners. run out of time, gotta go bike riding with the kiddies... $AUD 0.02 Matt Hurd.

Matt Hurd wrote:
On Fri, 16 Jul 2004 13:54:32 -0400, Michael Glassford <glassfordm@hotmail.com> wrote:
In an attempt to reconcile the different preferences and ideas expressed in this thread, here's a thought experiment for mutex & lock unification. What if the mutex and lock classes were policy-based, allowing users to choose the interface they want and supplying a typedef or two for the "most common" or "approved" variations?
Example below (note that I haven't tried to implement multiple policies elegantly). Hopefully it could be expanded to include recursive, checked, read/write, etc.
<snip>
//Example typedefs:
typedef mutex_type< blocking_mutex_policy, try_mutex_policy, fast_timed_mutex_policy
mutex;
typedef blocking_mutex_type< blocking_mutex_policy, null_policy, null_policy
mutex;
typedef try_mutex_type< try_mutex_policy, null_policy, null_policy,
mutex;
typedef timed_mutex_type< timed_mutex_policy, null_policy, null_policy,
mutex;
<snip>
//Example typedefs:
typedef lock_type< blocking_lock_policy, try_lock_policy, timed_lock_policy
lock;
typedef lock_type< blocking_lock_policy, null_policy, null_policy
blocking_lock;
typedef lock_type< try_lock_policy, null_policy, null_policy> try_lock;
typedef lock_type< timed_lock_policy, null_policy, null_policy
timed_lock;
I think this is thinking along the right track though I'm not fond of your interface above, but it is a good start to the thinking.
Do you mean having three template parameters? I mentioned that that was just for the sake of example. If you mean something else, I'm not sure what.
I've been reading over some of the old boost thread threads and I think I now see why Bill Kempf is now away from boost::thread. The best spin is, I think, he gave up on consensus.
This occurred to me during this massive threading thread. It seems that no two people agree on what a threading library should look like (e.g. some people want one mutex class and one lock class; others want three mutex classes and three lock classes with no overlap; others want three of each, with each being a superset of the last; others like things the way they are now; etc.). I thought perhaps that well designed policy-based classes could give most people what they want without making everyone else have it too.
It it interesting the old ground we are covering, some of the thinking back then was perhaps a little better then. Some of the current ideas seem a little better too.
Yes, many of the same discussions seem to get rehashed, don't they? Much like they do with smart pointers, oddly enough.
I do like David Held's thinking, perhaps because it is similar to mine ;-), in these mails: http://aspn.activestate.com/ASPN/Mail/Message/boost/1213138 http://aspn.activestate.com/ASPN/Mail/Message/boost/1207304
Bill Kempf was concerned about providing as safe as possible semantics for locking. He saw the need for lock transfers especially for Bjarne's auto locking proxy idiom. One way of doing this was to expose lock and unlock on a lock, but he felt exposing the same on the mutex was safer
I read things the opposite way. At the very beginning he wanted no lock an unlock methods on the lock, but changed his mind or at least conceded the argument; however, he thought (and I agree) that lock and unlock methods on the mutex were dangerous. Did you mis-type, or could you give references?
yet still troubling. Bill related transfer semantics to auto_ptr and saw the shared_ptr approach as problematic.
Others are of the opinion that lock and unlock is fine and catering threading newbies should not prevent them from the low level tools that makes their life fun ;-)
I side with Bill, on balance, I think on this. I see public a lock and unlock as the equivalent of a threading "goto".
However, I'm not so sure about his shared pointer reluctance at one level. His argument was that a shared_pointer interface requirements are a lot different to an interface for a lock with move/transfer semantics, which is true. He also argued that sharing a lock made no sense and was dangerous.
I agree that sharing a lock makes no sense; explicit transfer (not sharing) of a locks seems to make sense in some circumstances, though. I think Bill came to the conclusion that transferable locks were something he should do, and that most/all of the people participating in the discussion agreed.
This is kind of true, but we do share a mutex with a shareable mutex (the one i keep calling a shareable mutex and the world calls a read write mutex).
A non-shareable mutex in effect would have a count of zero or one (true or false) and ownership transfer would be the correct interface. Careful consideration must go to thinking about such situations as : do_something( lock_object ); w.r.t. to transfer. Perhaps lock_object = do_something( lock_object ); covers the case, but I can see the danger already...
There are grounds for concern over the implementation delivery of shared / transfer implementation as it should either rely on no-locking, atomic primitives or locking primitives below the normal locking to avoid the chicken and the egg of locking a count.
I'm not sure I understand what you're getting at. To make sure we're talking about the same thing: 1) I'm not talking about transfer of locks between threads. 2) I am talking about transferring locks from one scope to another scope (e.g. out of a function) within the context of a single thread. 3) This is essentially move construction or move assignment (that is, the internal state of one lock--e.g. the mutex reference and lock state--is copied into another lock, then the first lock clears its state so that it doesn't try to unlock the mutex or whatever). It's a transfer of ownership. 4) I'm not talking about transfer = lock promotion or demotion. Though the syntax is the same, it's a different operation (ownership is transferred, but the lock type is changed as well). That's not to say that I disagree with that syntax for promotion or demotion, just that that's not what I mean when I talk about lock transfer.
I would like to see David Held's RIIA mechanism, mixed with Michael's policy approach to deliver a solution that could also be consistent with the resource acquisition requirements of socket and file i/o. Timer interaction, especially because or try_lock, where the timer is a resource should perhaps be covered too.
The mutex taxonomy I have in mind would be: 1. null_mutex elided ops, atomic ops with interlocking
This is a good idea.
2. simple_mutex basic os mutex / critical section recursive on windows, non-recursive on posix 3. recursive_mutex recursive always 4. shareable_mutex supports exclusive and shared access reflects os primitives where available ( what does this mean with recursiveness???) 5. upgradeable_shareable_mutex adds upgradeable read lock equivalent
yeah, maybe not the best names
- a lock is a resource acquisition on a mutex. - avoid lock and unlock methods if possible via move/transfer semantics - consider a read lock as a shared_lock, lots of owners.
run out of time, gotta go bike riding with the kiddies...
$AUD 0.02
Matt Hurd.
Mike

On Fri, 16 Jul 2004 23:38:43 -0400, Michael Glassford <glassfordm@hotmail.com> wrote:
Matt Hurd wrote:
On Fri, 16 Jul 2004 13:54:32 -0400, Michael Glassford <glassfordm@hotmail.com> wrote:
In an attempt to reconcile the different preferences and ideas expressed in this thread, here's a thought experiment for mutex & lock unification. What if the mutex and lock classes were policy-based, allowing users to choose the interface they want and supplying a typedef or two for the "most common" or "approved" variations?
Example below (note that I haven't tried to implement multiple policies elegantly). Hopefully it could be expanded to include recursive, checked, read/write, etc.
<snip>
//Example typedefs:
typedef mutex_type< blocking_mutex_policy, try_mutex_policy, fast_timed_mutex_policy
mutex;
typedef blocking_mutex_type< blocking_mutex_policy, null_policy, null_policy
mutex;
typedef try_mutex_type< try_mutex_policy, null_policy, null_policy,
mutex;
typedef timed_mutex_type< timed_mutex_policy, null_policy, null_policy,
mutex;
<snip>
//Example typedefs:
typedef lock_type< blocking_lock_policy, try_lock_policy, timed_lock_policy
lock;
typedef lock_type< blocking_lock_policy, null_policy, null_policy
blocking_lock;
typedef lock_type< try_lock_policy, null_policy, null_policy> try_lock;
typedef lock_type< timed_lock_policy, null_policy, null_policy
timed_lock;
I think this is thinking along the right track though I'm not fond of your interface above, but it is a good start to the thinking.
Do you mean having three template parameters? I mentioned that that was just for the sake of example. If you mean something else, I'm not sure what.
Not sure about the way you've done the template parameters. I haven't given it enough thought to comment much, I'll try to find some time in the next couple of daze to give it some diligence.
I've been reading over some of the old boost thread threads and I think I now see why Bill Kempf is now away from boost::thread. The best spin is, I think, he gave up on consensus.
This occurred to me during this massive threading thread. It seems that no two people agree on what a threading library should look like (e.g. some people want one mutex class and one lock class; others want three mutex classes and three lock classes with no overlap; others want three of each, with each being a superset of the last; others like things the way they are now; etc.). I thought perhaps that well designed policy-based classes could give most people what they want without making everyone else have it too.
Yes, I think it is going to be the only way to provide basic locking functionality that is not easily abused and the full low level access that others strive for. There are a bunch of clever people with strong views on needing different things and a policy based approach maybe the only way to deliver this. Basic typedefs for scoped locks and the standard mutexes will do a 95% job I'd think.
It it interesting the old ground we are covering, some of the thinking back then was perhaps a little better then. Some of the current ideas seem a little better too.
Yes, many of the same discussions seem to get rehashed, don't they? Much like they do with smart pointers, oddly enough.
Sometimes this is good, I might understand what is going on the fourth time around ;-) Documents and wiki's are often better than newsgroups at this as the sense of history is better.
I do like David Held's thinking, perhaps because it is similar to mine ;-), in these mails: http://aspn.activestate.com/ASPN/Mail/Message/boost/1213138 http://aspn.activestate.com/ASPN/Mail/Message/boost/1207304
Bill Kempf was concerned about providing as safe as possible semantics for locking. He saw the need for lock transfers especially for Bjarne's auto locking proxy idiom. One way of doing this was to expose lock and unlock on a lock, but he felt exposing the same on the mutex was safer
I read things the opposite way. At the very beginning he wanted no lock an unlock methods on the lock, but changed his mind or at least conceded the argument; however, he thought (and I agree) that lock and unlock methods on the mutex were dangerous. Did you mis-type, or could you give references?
I think we are agreeing in a round about way... I'm not the clearest typer. Yes, he does concede the need for the equivalent of lock and unlock but saw the transfer of ownership as the better way rather than explicit lock and unlock as you indicate. I think. He covered the mutex locking thing here, saying he considered it but thought it was a bad idea. http://aspn.activestate.com/ASPN/Mail/Message/boost/1207226 <quote> It's the second problem that's more difficult. I originally thought I'd = solve this solution by creating some sort of lock_operations<> template = that would expose a Mutex's lock operations externally, thus reducing = the likelihood that a user would abuse the use of the lock operations = solely because they were readily available in the Mutex's interface. = However, this isn't really a great solution. For one thing it just = feels like a hack, and the end result is still an interface that's = easily misused. </quote>
yet still troubling. Bill related transfer semantics to auto_ptr and saw the shared_ptr approach as problematic.
Others are of the opinion that lock and unlock is fine and catering threading newbies should not prevent them from the low level tools that makes their life fun ;-)
I side with Bill, on balance, I think on this. I see public a lock and unlock as the equivalent of a threading "goto".
However, I'm not so sure about his shared pointer reluctance at one level. His argument was that a shared_pointer interface requirements are a lot different to an interface for a lock with move/transfer semantics, which is true. He also argued that sharing a lock made no sense and was dangerous.
I agree that sharing a lock makes no sense; explicit transfer (not sharing) of a locks seems to make sense in some circumstances, though.
I think Bill came to the conclusion that transferable locks were something he should do, and that most/all of the people participating in the discussion agreed.
Yes. However, thinking about what a read lock is, it is really just a reference counted lock in some respects... In this light is it not just the same as sharing a lock? Kind of anyway, it is really sharing the ownership of the mutex, which has the colloquial form "lock sharing".
This is kind of true, but we do share a mutex with a shareable mutex (the one i keep calling a shareable mutex and the world calls a read write mutex).
A non-shareable mutex in effect would have a count of zero or one (true or false) and ownership transfer would be the correct interface. Careful consideration must go to thinking about such situations as : do_something( lock_object ); w.r.t. to transfer. Perhaps lock_object = do_something( lock_object ); covers the case, but I can see the danger already...
There are grounds for concern over the implementation delivery of shared / transfer implementation as it should either rely on no-locking, atomic primitives or locking primitives below the normal locking to avoid the chicken and the egg of locking a count.
I'm not sure I understand what you're getting at. To make sure we're talking about the same thing:
1) I'm not talking about transfer of locks between threads.
Agreed, but with the read/shared lock exception to the rule... This is a bad idea. The case I gave previously still stands where is stuffs up the thinking with respect to sharing a mutex with multiple objects. Which is at least one example where transferring locks across threads can cause grief if allowed. This could be overcome by a policy that takes care of the mutex / thread_id housekeeping to make this work. However, semantically a read lock is sharing a lock amongst many threads which confuses the issue.
2) I am talking about transferring locks from one scope to another scope (e.g. out of a function) within the context of a single thread.
Yes, this should be done with the transfer / move semantics.
3) This is essentially move construction or move assignment (that is, the internal state of one lock--e.g. the mutex reference and lock state--is copied into another lock, then the first lock clears its state so that it doesn't try to unlock the mutex or whatever). It's a transfer of ownership.
Agreed.
4) I'm not talking about transfer = lock promotion or demotion. Though the syntax is the same, it's a different operation (ownership is transferred, but the lock type is changed as well). That's not to say that I disagree with that syntax for promotion or demotion, just that that's not what I mean when I talk about lock transfer.
Agreed. Yes, I can see the case for an upgradeable read lock but the circumstance strikes me as unusually rare and I would like to see it as a different mutex and not pollute a normal shareable (rw) mutex.
I would like to see David Held's RIIA mechanism, mixed with Michael's policy approach to deliver a solution that could also be consistent with the resource acquisition requirements of socket and file i/o. Timer interaction, especially because or try_lock, where the timer is a resource should perhaps be covered too.
The mutex taxonomy I have in mind would be: 1. null_mutex elided ops, atomic ops with interlocking
This is a good idea.
Uh oh, I mistyped that in my rush to go riding with my kids and you are agreeing with me.... I think the null mutex should have inc, dec, exchange and whatever as static non-interlocked ops and the other mutexes should have them as static interlocked ops. That keeps a null mutex to the single threaded case. Perhaps interlocked ops should live in a multi_thread:: or single_threaded:: struct and the appropriate mutexes should just inherit these interfaces. That way seems neater as you can still reference the multi_threaded::add(...) and the Mutex::add(...) for the appropriate context sensitive operations.
2. simple_mutex basic os mutex / critical section recursive on windows, non-recursive on posix 3. recursive_mutex recursive always 4. shareable_mutex supports exclusive and shared access reflects os primitives where available ( what does this mean with recursiveness???) 5. upgradeable_shareable_mutex adds upgradeable read lock equivalent
yeah, maybe not the best names
- a lock is a resource acquisition on a mutex. - avoid lock and unlock methods if possible via move/transfer semantics - consider a read lock as a shared_lock, lots of owners.
run out of time, gotta go bike riding with the kiddies...
$AUD 0.02
Matt Hurd.
Mike
I'll have a look at your interface and put some of my own thoughts in a code form in the next few daze. Nice chatting, regards, Matt Hurd.

Matt Hurd wrote:
On Fri, 16 Jul 2004 23:38:43 -0400, Michael Glassford <glassfordm@hotmail.com> wrote:
Matt Hurd wrote:
On Fri, 16 Jul 2004 13:54:32 -0400, Michael Glassford <glassfordm@hotmail.com> wrote:
[big snip]
The mutex taxonomy I have in mind would be: 1. null_mutex elided ops, atomic ops with interlocking
This is a good idea.
Uh oh, I mistyped that in my rush to go riding with my kids and you are agreeing with me....
I think the null mutex should have inc, dec, exchange and whatever as static non-interlocked ops and the other mutexes should have them as static interlocked ops.
That keeps a null mutex to the single threaded case.
Perhaps interlocked ops should live in a multi_thread:: or single_threaded:: struct and the appropriate mutexes should just inherit these interfaces.
That way seems neater as you can still reference the multi_threaded::add(...) and the Mutex::add(...) for the appropriate context sensitive operations.
Actually, all I meant is that I liked the idea of a null mutex (in the context of it being substituted for a real mutex in a single-threaded application). [another snip] Mike

On Sat, 17 Jul 2004 11:42:18 -0400, Michael Glassford <glassfordm@hotmail.com> wrote:
Matt Hurd wrote: [snip] Actually, all I meant is that I liked the idea of a null mutex (in the context of it being substituted for a real mutex in a single-threaded application).
Kewl. I think that will be a nice small implementation and large conceptual step forward. Another big step forward might be being able to write to a shareable mutex style (rw) and have it work with (by being substitutable for) the other mutex types, policy or not (hopefully with policies). This will make one code base suitable for null_mutex ... shareable_mutex (rw) if you write to the shareable (rw) case. I think we are already largely headed down that line. The main trick here is making shared / exclusive access (r/w) at the lock interface map to just exclusive for the exclusive only mutexes. The other facet is accepting and ignoring the shared / exclusive priorities, e.g. interleaved, exclusive_priority (write priority), etc on mutex construction for the base mutex types. This is a necessarily different approach to having compile time errors for features that are not appropriate. Compile time errors are still appropriate other features, such as timed locks which may not be available due to config/policy. I do have this dream of the types being able to minimally configure themselves based on their use which would be appropriate for some design choices... think of it as a reverse policy approach, but that is research not dev... AFAIK Regards, Matt Hurd.

Matt Hurd wrote:
On Fri, 16 Jul 2004 23:38:43 -0400, Michael Glassford <glassfordm@hotmail.com> wrote:
This occurred to me during this massive threading thread. It seems that no two people agree on what a threading library should look like (e.g. some people want one mutex class and one lock class; others want three mutex classes and three lock classes with no overlap; others want three of each, with each being a superset of the last; others like things the way they are now; etc.). I thought perhaps that well designed policy-based classes could give most people what they want without making everyone else have it too.
Yes, I think it is going to be the only way to provide basic locking functionality that is not easily abused and the full low level access that others strive for.
There are a bunch of clever people with strong views on needing different things and a policy based approach maybe the only way to deliver this. Basic typedefs for scoped locks and the standard mutexes will do a 95% job I'd think.
The only thing I fear (and this is just me being paranoid) is that if we put out too many different interfaces to the same functionality, it will make it that much harder for new users to learn, and that much easier to get wrong. I recognize that developers are going to use the interface that's easiest for them to write. Even if there's an (argueably) safer way to use it, it's not going to happen if the interface provides shortcuts. So, despite the fact that my strong views favor safety over convenience, I feel that I'd rather concede the point and see a clean, minimal interface than have a complex interface to support every usage paradigm, particularly if it's only going to support special interests like my own. Christopher -- Christopher Currie <codemonkey@gmail.com>

Matt Hurd <matt.hurd@gmail.com> writes:
I've been reading over some of the old boost thread threads and I think I now see why Bill Kempf is now away from boost::thread. The best spin is, I think, he gave up on consensus.
Really? I don't recall any significant threading traffic just before he disappeared. -- Dave Abrahams Boost Consulting http://www.boost-consulting.com
participants (11)
-
Batov, Vladimir
-
Bronek Kozicki
-
Christopher Currie
-
Daniel Wallin
-
David Abrahams
-
Eric Niebler
-
Howard Hinnant
-
Matt Hurd
-
Michael Glassford
-
Peter Dimov
-
Rob Stewart