
On Aug 7, 2004, at 2:41 PM, Alexander Terekhov wrote:
Howard Hinnant wrote: ...
void read_write(rw_mutex& m, which_t w) { sharable_lock<rw_mutex, true> read_lock(m); // May need access to many releated which_t's if (compute_expensive_result(w)) { // Upgrade (blocks upcoming readers while pending) scoped_lock<rw_mutex> write_lock(upgrade(read_lock)); if (read_lock.atomic_upgrade()) { modify_state(w); if (write_lock.upgrade_pending()) register_change(w); } else if (!computation_invalidated(w) || // check registry compute_expensive_result(w)) { modify_state(w); write_lock.upgrade_pending() ? register_change(w) : clear_registry(); } else if (!write_lock.upgrade_pending()) { clear_registry(); } } }
Oder (verbosity aside for a moment)?
My current proposal ( http://home.twcny.rr.com/hinnant/cpp_extensions/threads.html ) already has the functionality of "fallible upgrade" but with slightly different semantics and syntax: scoped_lock<rw_mutex> write_lock(upgrade(read_lock)); if (read_lock.atomic_upgrade()) { ... } else { ... } vs: scoped_lock<rw_mutex> write_lock(move(read_lock), try_lock); if (write_lock.locked()) { ... } else { read_lock.unlock(); write_lock.lock(); ... } Hmm... ok the functionality isn't exactly the same as you proposed. But it's pretty close. The difference is your upgrade might block for a while during the try-upgrade operation in the hopes of making it atomic, whereas mine will give up immediately and then block for a non-atomic upgrade. See further down on how I see your block-try semantics as counter productive in another scenario. So I believe you could design code at least partly the way you've proposed with my current proposal. Although I'm sure I'm not fully understanding the write_lock.upgrade_pending() functionality. The way I currently have my rw_mutex implemented, this information is not known. Actually my current implementation is based on your suggested "single entry gate" design. So when the mutex is write locked, it has no idea who or if anyone is waiting outside the gate. I liked the idea that the scheduler, not the mutex, decides if a reader or writer gets priority. Anyway, I could see a "write lock pending" in the case that the mutex is currently in a read state. But only a sharable (non-upgradable) lock could ever see that state. Once an upgradable lock is active, even though it hasn't requested an upgrade, writers are locked behind the entry gate and not detectable. And once a writer gets past the entry gate and is "pending", then upgradable locks are stuck behind the entry gate. But I believe that in addition to the fallible upgrade there needs to be a "for-sure upgrade". You don't always want to have to check for whether you got your upgrade (atomically or otherwise). There is a code size hit in having to write the "else" branch. And it would also become problematic to atomically pair an upgrade with an independent lock. For example consider a function that takes two objects T and U, needs to read T, and if certain conditions are met then atomically upgrade T access from read to write, and lock U: void foo(T& t, U& u) { typedef T::mutex_t::upgradable_lock T_ReadLock; typedef T::mutex_t::scoped_lock T_WriteLock; typedef transfer_lock< T_WriteLock, T_ReadLock > T_Upgrade; typedef U::mutex_t::scoped_lock U_Lock; // read lock t T_ReadLock t_read_lock(t.mutex()); ... if (...) { // upgrade t from read to write, and lock u T_WriteLock t_write_lock(t.mutex(), defer_lock); T_Upgrade t_upgrade(t_write_lock, t_read_lock, defer_lock); U_Lock u_lock(u.mutex(), defer_lock); lock(t_upgrade, u_lock); // ok, t upgraded and u locked, all atomically } } The transfer_lock<T_WriteLock, T_ReadLock> generic utility can make an upgrade look like a lock, but it really needs the "for sure atomic upgrade" semantics in order to pull it off. And it also needs "try but retain read access on fail" semantics. This is in contrast to your "try but do non-atomic upgrade on fail" semantics. And then the lock(lock1,lock2) generic algorithm can atomically do the upgrade on t and lock on u. Having said all that, a word of caution on the above example. If U::mutex is a simple exclusive mutex, things are ok. However if U::mutex is a read/write style mutex, then the above algorithm is in danger of deadlock. If foo2(U& u, T& t) does the same thing as foo(T& t, U& u), but with the roles reversed: read-lock u, atomically(upgrade u, lock t), then we're firmly in deadlock territory. Threads simultaneously executing foo and foo2 would deadlock for sure. The above paragraph is an argument against all-in-one mutex functionality that I hadn't thought of before (at least if you consider "all-in-one" to include read/write capability). If a mutex existed, and did not include read/write capability, I suspect that the above foo could check that fact for U::mutex_t at compile time, and thus ensure that there was no danger of deadlock. -Howard