(Note: Much of this is redundant with commentary on Massive CPU load using std::lock (c++11), but I think this topic deserves its own question and answers.)
I recently encountered some sample C++11 code that looked something like this:
std::unique_lock<std::mutex> lock1(from_acct.mutex, std::defer_lock);
std::unique_lock<std::mutex> lock2(to_acct.mutex, std::defer_lock);
std::lock(lock1, lock2); // avoid deadlock
transfer_money(from_acct, to_acct, amount);
Wow, I thought, std::lock
sounds interesting. I wonder what the standard says it does?
C++11 section 30.4.3 [thread.lock.algorithm], paragraphs (4) and (5):
template void lock(L1&, L2&, L3&...);
4 Requires: Each template parameter type shall meet the Lockable requirements, [ Note: The
unique_lock
class template meets these requirements when suitably instantiated. — end note ]5 Effects: All arguments are locked via a sequence of calls to
lock()
,try_lock()
, orunlock()
on each argument. The sequence of calls shall not result in deadlock, but is otherwise unspecifed. [ Note: A deadlock avoidance algorithm such as try-and-back-off must be used, but the specifc algorithm is not specifed to avoid over-constraining implementations. — end note ] If a call tolock()
ortry_lock()
throws an exception,unlock()
shall be called for any argument that had been locked by a call tolock()
ortry_lock()
.
Consider the following example. Call it "Example 1":
Thread 1 Thread 2
std::lock(lock1, lock2); std::lock(lock2, lock1);
Can this deadlock?
A plain reading of the standard says "no". Great! Maybe the compiler can order my locks for me, which would be kind of neat.
Now try Example 2:
Thread 1 Thread 2
std::lock(lock1, lock2, lock3, lock4); std::lock(lock3, lock4);
std::lock(lock1, lock2);
Can this deadlock?
Here again, a plain reading of the standard says "no". Uh oh. The only way to do that is with some kind of back-off-and-retry loop. More on that below.
Finally, Example 3:
Thread 1 Thread 2
std::lock(lock1,lock2); std::lock(lock3,lock4);
std::lock(lock3,lock4); std::lock(lock1,lock2);
Can this deadlock?
Once again, a plain reading of the standard says "no". (If the "sequence of calls to lock()
" in one of these invocations is not "resulting in deadlock", what is, exactly?) However, I am pretty sure this is unimplementable, so I suppose it's not what they meant.
This appears to be one of the worst things I have ever seen in a C++ standard. I am guessing it started out as an interesting idea: Let the compiler assign a lock ordering. But once the committee chewed it up, the result is either unimplementable or requires a retry loop. And yes, that is a bad idea.
You can argue that "back off and retry" is sometimes useful. That is true, but only when you do not know which locks you are trying to grab up front. For example, if the identity of the second lock depends on data protected by the first (say because you are traversing some hierarchy), then you might have to do some grab-release-grab spinning. But in that case you cannot use this gadget, because you do not know all of the locks up front. On the other hand, if you do know which locks you want up front, then you (almost) always want simply to impose an ordering, not to loop.
Also, note that Example 1 can live-lock if the implementation simply grabs the locks in order, backs off, and retries.
In short, this gadget strikes me as useless at best. Just a bad idea all around.
OK, questions. (1) Are any of my claims or interpretations wrong? (2) If not, what the heck were they thinking? (3) Should we all agree that "best practice" is to avoid std::lock
completely?
[Update]
Some answers say I am misinterpreting the standard, then go on to interpret it the same way I did, then confuse the specification with the implementation.
So, just to be clear:
In my reading of the standard, Example 1 and Example 2 cannot deadlock. Example 3 can, but only because avoiding deadlock in that case is unimplementable.
The entire point of my question is that avoiding deadlock for Example 2 requires a back-off-and-retry loop, and such loops are extremely poor practice. (Yes, some sort of static analysis on this trivial example could make that avoidable, but not in the general case.) Also note that GCC implements this thing as a busy loop.
[Update 2]
I think a lot of the disconnect here is a basic difference in philosophy.
There are two approaches to writing software, especially multi-threaded software.
In one approach, you throw a bunch of stuff together and run it to see how well it works. You are never convinced that your code has a problem unless someone can demonstrate that problem on a real system, right now, today.
In the other approach, you write code that can be rigorously analyzed to prove that it has no data races, that all of its loops terminate with probability 1, and so forth. You perform this analysis strictly within the machine model guaranteed by the language spec, not on any particular implementation.
Advocates of the latter approach are not impressed by any demonstrations on particular CPUs, compilers, compiler minor versions, operating systems, runtimes, etc. Such demonstrations are barely interesting and totally irrelevant. If your algorithm has a data race, it is broken, no matter what happens when you run it. If your algorithm has a livelock, it is broken, no matter what happens when you run it. And so forth.
In my world, the second approach is called "Engineering". I am not sure what the first approach is called.
As far as I can tell, the std::lock
interface is useless for Engineering. I would love to be proven wrong.