42

(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(), or unlock() 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 to lock() or try_lock() throws an exception, unlock() shall be called for any argument that had been locked by a call to lock() or try_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.

Community
  • 1
  • 1
Nemo
  • 70,042
  • 10
  • 116
  • 153
  • I am no expert, but I think that case 2 can be implemented without busy-loop (maybe someone can confirm?). As for case 3, I think that the standard allows that to deadlock (you are probably mistaking std::lock for Lockable::lock() in par.5). – sbabbi Aug 29 '13 at 21:17
  • @sbabbi: In general, Example 2 cannot be implemented without a busy loop. (In particular, if Thread 2 chooses a lock order based on a run-time calculation, there is no way Thread 1 can avoid deadlock without some sort of back-off-and-retry algorithm.) – Nemo Aug 29 '13 at 21:27
  • Did the code you post compile? `std::lock(lock1);` doesn't compile for me. Also, did you compile, run and test the code I posted at http://stackoverflow.com/a/14525010/576911, including the implementation of `lock(x, y)` in the update section? – Howard Hinnant Aug 29 '13 at 21:41
  • @HowardHinnant: OK so it requires at least two arguments. Does not change the essence of the question. Fixed. Also your implementation can obviously live-lock on Example 1. (Just imagine the two threads running in lockstep; they will keep banging into each other and retrying. `yield` reduces the load but does not fix the problem.) – Nemo Aug 29 '13 at 21:47
  • 3
    Code it up and try to get it to live-lock. – Howard Hinnant Aug 29 '13 at 21:59
  • 6
    @HowardHinnant: That proves exactly nothing. You can run a program a trillion times and never trigger a race condition; that does not prove there is no race condition. Simply imagine two threads running Example 1 concurrently, going through your code _in lockstep_, line for line; i.e. both threads on the same line of code at all times. (Of course, in real life scheduling jitter will probably break the livelock eventually. That does not mean your _algorithm_ does not suffer from livelock. It obviously does...) – Nemo Aug 29 '13 at 22:08
  • 3
    The live-lock state has very positive eigenvalues. If a live-lock state did form, it would fall apart very quickly. It would not be a stable state. – Howard Hinnant Aug 29 '13 at 22:11
  • 1
    "*Example 3 can, but only because the natural reading is unimplementable.*" ... where is *that* coming from? – Nicol Bolas Aug 29 '13 at 22:19
  • 6
    -1 because of your comments. You seem to only want to rant than resolve the question. – GManNickG Aug 29 '13 at 22:46
  • The standard specifies "The sequence of calls shall not result in deadlock," which is not the same as saying that calling `std::lock` shall not deadlock. A reasonable reading is "If there is any sequence that could succeed then `std::lock` will succeed." Example 3 can deadlock, but not due to the sequencing of calls within any invocation of `std::lock`. – bames53 Aug 29 '13 at 23:27
  • 2
    "And yes, that is idiotic. If enough developers decided to use this gadget in their code by default, it would bring the system to its knees." I think you need to defend that claim. In the question you linked to the CPU load was from a poor implementation, and using a proper implementation cleared the load problem right up. – bames53 Aug 29 '13 at 23:47
  • @NicolBolas: Rephrased. – Nemo Aug 29 '13 at 23:48
  • 1
    @GManNickG: Actually I am honestly curious what the standard author(s) intended here. Yes I am of the opinion that back-off + retry strategies are a bad idea, which I actually thought was common knowledge. – Nemo Aug 29 '13 at 23:49
  • @bames53: See my comments on HowardHinnant's answer. I think if you generalize his approach to N locks, livelock becomes increasingly likely as N grows. In my experience, no skilled multi-threading programmer would choose a back-off + retry algorithm over lock ordering given the choice. Apparently my experience is atypical (?) – Nemo Aug 29 '13 at 23:53
  • @Nemo, you state in your latest edit that example 3 can't deadlock but that's not what you posited originally. In fact the text is still there, stating that a plain reading says it can't (maybe that's just some miscommunication). That's the bit that people have been answering. – paxdiablo Aug 30 '13 at 00:03
  • @Nemo, as to your comment that back-offs are problematic, you're absolutely correct and we can all agree on that. But, if you want the deadlock-avoiding behaviour, it's the only way to do it for all scenarios. Yes, ordering your locks the same way every time will make the deadlock-avoidance unnecessary but that may not always be possible. By all means use consistent ordering which guarantees no possible deadlock but backoff/retry is there for you _if you need it._ – paxdiablo Aug 30 '13 at 00:04
  • @paxdiablo: (1) I think the plain reading is unimplementable for Example 3, and therefore could not be the intention. (I did try to say this in the main text, but I guess I phrased it badly.) I am still not 100% sure about the intention for Example 2, which you yourself changed your mind over :-). (2) As stated in the Q, if you know all locks you need all at once, you absolutely can (and should) impose an order on them. If you do not know, then this interface is not usable at all. Thus any time you _can_ use it, it seems to me you _should not_, which is why I call it a bad spec. – Nemo Aug 30 '13 at 02:32
  • 1
    @GManNickG: Edited to tone down my language. – Nemo Aug 30 '13 at 02:37
  • @Nemo I believe your 'plain reading' is incorrect; it's not simply that what is written can't be implemented, it's that you have misread what is written. The spec say that deadlocks shall not occur due to the implementation choosing one particular sequence of calls rather than some different sequence that would have worked. Example 3 may deadlock when there is no sequence of calls that can succeed; In that case the deadlock is not due to sequence of calls used. At least that's my reading of the spec. – bames53 Aug 30 '13 at 04:34
  • @bames53: Precise language would say that a sequence of calls _has_ an order, not that it _is_ an order. The spec literally states "the sequence of calls shall not result in deadlock". Not "shall not cause deadlock", but "shall not _result in_ deadlock". Here we see a sequence of calls, with deadlock as the result; eliminate the sequence, eliminate the deadlock. You say, "But here all sequences of calls would result in deadlock, so that cannot be what they meant." And I agree it is not what they _meant_, but it is quite precisely what they _said_. This is one reason I consider this a bad spec. – Nemo Aug 30 '13 at 14:18
  • 1
    The C++ standard is an open source project. You can rant or you can help: http://cplusplus.github.io/LWG/lwg-active.html#submit_issue – Howard Hinnant Aug 30 '13 at 14:35
  • 1
    I say that there are multiple interpretations of the words and you've chosen incorrect ones. Like if I say "Bob's gone to the bank," and you point and say "No, he's gone to the edge of that brook; There's no financial institution there." Here the ambiguous sentence is "The sequence of calls shall not result in deadlock," and you seem to be interpreting it as "no deadlock whatsoever shall occur during this sequence," when you should understand it to mean "a deadlock shall not be the consequence of the choice of sequence, but may occur due to other factors." @Nemo – bames53 Aug 30 '13 at 14:36
  • @Nemo The deadlock in example three is not the result of _the sequence_, but of another factor. – bames53 Aug 30 '13 at 14:42
  • If `std::lock` analyzed the graph of locked `unique_lock`s held by each thread in the event of a seeming deadlock, it could avoid back-off-and-retry by (attempting to) prove what back-off is required in order to proceed, at least in the naive case where every `std::lock` is only being passed `unique_lock`s. For more strange locks, back-off-and-retry may be required. And, for the most extreme cases (#3) where there is no locking solution, infinite livelock may be required by the standard (so long as you fail to deadlock, you are following the standard). Am I missing something? – Yakk - Adam Nevraumont Aug 30 '13 at 14:49
  • @bames53: One last try and you can have the last word. "Action A resulted in B." "How can you tell?" "Because if I did not do A, B would not have happened." This is nearly a tautology. Of course deadlock has multiple causes. Nonetheless, the sequence of calls is clearly _resulting in_ deadlock. Notice how I keep quoting the spec's literal words while you keep rephrasing them. It does not say "the order shall not result" nor "the choice of sequence shall not result" nor "deadlock due to" nor "caused by" nor anything of the kind. It says the _sequence of calls shall not result in deadlock_. – Nemo Aug 30 '13 at 14:51
  • @Nemo I'm using different words so that the meaning is not ambiguous. It's true that "the sequence of calls results in a deadlock," as in "when these steps are performed a deadlock occurs." But "the sequence of calls results in a deadlock," as in "the order these steps are performed in results in a deadlock," is false. In example 3 is is not the sequence which results in the deadlock; The deadlock results from the fact that one of the steps in the sequence will deadlock regardless of the ordering. – bames53 Aug 30 '13 at 15:15
  • @bames53: (Last reply for real.) "The order the steps are performed" (or "the choice of steps") is clearly what they meant, but it equally clearly not what they said. I honestly do not see how anyone could claim this spec clearly distinguishes Example 2 from Example 3. As written, the language is ambiguous at best... Which is itself a bug in the spec. – Nemo Aug 30 '13 at 15:20
  • @Nemo "sequence" is a synonym for "order", so that really is what they wrote. It is ambiguous, all natural language is, no matter how hard we try. The standard can obviously be improved, but that's separate from trying to understand its meaning. – bames53 Aug 30 '13 at 15:41
  • 1
    @Nemo if the spec says something seemingly ambiguous about a requirement of implementations, and one of the two interpretations is provably impossible, then there is only one way to implement the spec, so the implementation instructions are not ambiguous... – Yakk - Adam Nevraumont Aug 31 '13 at 03:56
  • 3
    @Nemo, I'm still not sure you're understanding what the sequence is. `30.4.3` clearly states, for a _single_ call to `std::lock`: **All arguments are locked via a sequence of calls to lock(), try_lock(), or unlock() on each argument. The sequence of calls shall not result in deadlock.** There is no deadlock avoidance across _multiple_ calls to `std::lock` because that's outside the sequence being discussed in this section. – paxdiablo Aug 31 '13 at 04:10
  • @paxdiablo: Even Example 1 involves "multiple" calls to `std::lock`. You can never have deadlock at all except "across multiple calls to `std::lock`". (With a single call, there can be no deadlock to avoid.) Example 2 even more clearly involves _multiple_ calls to `std::lock`. What is in the spec's wording, exactly, to distinguish Example 2 from Example 3? In the _wording itself_, not some artificial rephrasing? In all three, what I see is a sequence of calls with deadlock as a potential result... Except the spec forbids it, which is implementable for only two of the three. – Nemo Aug 31 '13 at 06:10
  • @Nemo, sorry, by multiple, I meant in the same thread. There is only one call to `std::lock` in thread 1 and one in thread 2. Thread 2 cannot deadlock because the four-lockable call from thread 1 is atomic. That's not so for example 3 since there are ordering issues across two threads - all the individual calls to `std::lock` are atomic but that's not the case for two separate calls. It's the _wording itself_ (as you put it) that states a single call cannot deeadlock as the _whole section_ is talking only about a single call to `std::lock`. – paxdiablo Aug 31 '13 at 06:30
  • @paxdiablo: Try a thought experiment. Suppose the spec did not mention deadlock avoidance and just required `std::lock` to lock arguments in order. Then any of these could deadlock. In Example 2, would you blame that deadlock entirely on Thread 1? I would say _all three_ calls to `std::lock` "resulted in" deadlock, equally, because removing any one would remove the deadlock. You blame it on Thread 1 alone because that is the only way the spec can be implementable. Again, a "single call" by itself can never deadlock; it always takes two or more. You are assigning blame to be kind to the spec. – Nemo Aug 31 '13 at 14:15
  • @paxdiablo: Put another way... In Example 3, every possible sequence results in deadlock, from which you want to infer that the deadlock is not the "result of" the sequence. But this is not plain English. Even if "every route leads to home", when I actually take a route, it _results in_ my reaching home. Replace "route" with "sequence" and "home" with "deadlock" to see what I am trying to say. – Nemo Aug 31 '13 at 15:12

4 Answers4

47

I think you are misunderstanding the scope of the deadlock avoidance. That's understandable since the text seems to mention lock in two different contexts, the "multi-lock" std::lock and the individual locks carried out by that "multi-lock" (however the lockables implement it). The text for std::lock states:

All arguments are locked via a sequence of calls to lock(), try_lock(),or unlock() on each argument. The sequence of calls shall not result in deadlock

If you call std::lock passing ten different lockables, the standard guarantees no deadlock for that call. It's not guaranteed that deadlock is avoided if you lock the lockables outside the control of std::lock. That means thread 1 locking A then B can deadlock against thread 2 locking B then A. That was the case in your original third example, which had (pseudo-code):

Thread 1     Thread 2
lock A       lock B
lock B       lock A

As that couldn't have been std::lock (it only locked one resource), it must have been something like unique_lock.

The deadlock avoidance will occur if both threads attempt to lock A/B and B/A in a single call to std::lock, as per your first example. Your second example won't deadlock either since thread 1 will be backing off if the second lock is needed by a thread 2 already having the first lock. Your updated third example:

Thread 1                  Thread 2
std::lock(lock1,lock2);   std::lock(lock3,lock4);
std::lock(lock3,lock4);   std::lock(lock1,lock2);

still has the possibility of deadlock since the atomicity of the lock is a single call to std::lock. For example, if thread 1 successfully locks lock1 and lock2, then thread 2 successfully locks lock3 and lock4, deadlock will ensue as both threads attempt to lock the resource held by the other.

So, in answer to your specific questions:

1/ Yes, I think you've misunderstood what the standard is saying. The sequence it talks about is clearly the sequence of locks carried out on the individual lockables passed to a single std::lock.

2/ As to what they were thinking, it's sometimes hard to tell :-) But I would posit that they wanted to give us capabilities that we would otherwise have to write ourselves. Yes, back-off-and-retry may not be an ideal strategy but, if you need the deadlock avoidance functionality, you may have to pay the price. Better for the implementation to provide it rather than it having to be written over and over again by developers.

3/ No, there's no need to avoid it. I don't think I've ever found myself in a situation where simple manual ordering of locks wasn't possible but I don't discount the possibility. If you do find yourself in that situation, this can assist (so you don't have to code up your own deadlock avoidance stuff).


In regard to the comments that back-off-and-retry is a problematic strategy, yes, that's correct. But you may be missing the point that it may be necessary if, for example, you cannot enforce the ordering of the locks before-hand.

And it doesn't have to be as bad as you think. Because the locks can be done in any order by std::lock, there's nothing stopping the implementation from re-ordering after each backoff to bring the "failing" lockable to the front of the list. That would mean those that were locked would tend to gather at the front, so that the std::lock would be less likely to be claiming resources unnecessarily.

Consider the call std::lock (a, b, c, d, e, f) in which f was the only lockable that was already locked. In the first lock attempt, that call would lock a through e then "fail" on f.

Following the back-off (unlocking a through e), the list to lock would be changed to f, a, b, c, d, e so that subsequent iterations would be less likely to unnecessarily lock. That's not fool-proof since other resources may be locked or unlocked between iterations, but it tends towards success.

In fact, it may even order the list initially by checking the states of all lockables so that all those currently locked are up the front. That would start the "tending toward success" operation earlier in the process.

That's just one strategy, there may well be others, even better. That's why the standard didn't mandate how it was to be done, on the off-chance there may be some genius out there who comes up with a better way.

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • 1
    In other words, only a back-off-and-retry implementation can comply with the standard as written? (Example 2 is the essence of my question. If we agree the standard forbids deadlock in Example 2, then I think we agree that the standard effectively mandates a back-off-and-retry busy loop. This is what GCC opted to do, by the way.) If so, then we are in violent agreement and this thing should clearly never be used in real life. – Nemo Aug 29 '13 at 21:58
  • 2
    @Nemo, there _may_ be other strategies that would work though I've not come across them in my LONG career :-) But that's why the standard didn't mandate how it was to be done, in case there's some uber-genius out there somewhere that will invent a new way. As to whether you use it, see my final paragraph. If you _do_ need backoff, better to let the library do it rather than code your own. However, I've never actually found such a situation. – paxdiablo Aug 29 '13 at 22:15
  • I am accepting this answer since it appears to be consensus. Note that _every_ possible implementation of this gadget suffers from livelock. (Certainly every proposed implementation does; I am pretty sure it is unavoidable in principle.) Implementations can reduce but never eliminate this failure potential, which is a very bad way to approach concurrent programming, in my opinion. So I still think `std::lock` is best avoided in scenarios where a superior approach exists... Which is _every_ scenario I (or you) can visualize. – Nemo Aug 31 '13 at 14:43
  • 2
    @Nemo - I've been developing multithreaded software for 30+ years, and have NEVER had to poll for a lock, not once. – Martin James May 10 '14 at 18:01
  • The only sane strategy I have come up with for the miserable case of complex multiple resource management is to ensure that no thread that requires a set of resources can be allowed to proceed unless ALL members of the set are available at the same time. This means blocking any thread that cannot get all its resources on a condvar or semaphore until other threads release the overlapping resources. – Martin James May 10 '14 at 18:06
  • Why is backoff-and-retry necessary? My naive understanding of deadlock is that it only occurs when locks are acquired in different order by different threads. Since we have the complete list of mutexes, why not order them by address and lock them in a consistent order? – Wheezil Aug 09 '20 at 15:30
  • @Wheezil, see my answer, the section detailing what happens when thread `1/2` tries to obtain locks `1-4` in separate calls. The sorting you mention can only happen on a *single* multi-lock lock call. – paxdiablo Aug 09 '20 at 20:23
25

Perhaps it would help if you thought of each individual call to std::lock(x, y, ...) as atomic. It will block until it can lock all of its arguments. If you don't know all of the mutexes you need to lock a-priori, do not use this function. If you do know, then you can safely use this function, without having to order your locks.

But by all means order your locks if that is what you prefer to do.

Thread 1                    Thread 2
std::lock(lock1, lock2);    std::lock(lock2, lock1);

The above will not deadlock. One of the threads will get both locks, and the other thread will block until the first one has released the locks.

Thread 1                                  Thread 2
std::lock(lock1, lock2, lock3, lock4);    std::lock(lock3, lock4);
                                          std::lock(lock1, lock2);

The above will not deadlock. Though this is tricky. If Thread 2 gets lock3 and lock4 before Thread1 does, then Thread 1 will block until Thread 2 releases all 4 locks. If Thread 1 gets the four locks first, then Thread 2 will block at the point of locking lock3 and lock4 until Thread 1 releases all 4 locks.

Thread 1                          Thread 2
std::lock(lock1,lock2);           std::lock(lock3,lock4);
std::lock(lock3,lock4);           std::lock(lock1,lock2);

Yes, the above can deadlock. You can view the above as exactly equivalent to:

Thread 1                          Thread 2
lock12.lock();                    lock34.lock();
lock34.lock();                    lock12.lock();

Update

I believe a misunderstanding is that dead-lock and live-lock are both correctness issues.

In actual practice, dead-lock is a correctness issue, as it causes the process to freeze. And live-lock is a performance issue, as it causes the process to slow down, but it still completes its task correctly. The reason is that live-lock will not (in practice) sustain itself indefinitely.

<disclaimer> There are forms of live-lock that can be created which are permanent, and thus equivalent to dead-lock. This answer does not address such code, and such code is not relevant to this issue. </disclaimer>

The yield shown in this answer is a significant performance optimization which significantly decreases live-lock, and thus significantly increases the performance of std::lock(x, y, ...).

Update 2

After a long delay, I have written a first draft of a paper on this subject. The paper compares 4 different ways of getting this job done. It contains software you can copy and paste into your own code and test yourself:

http://howardhinnant.github.io/dining_philosophers.html

Community
  • 1
  • 1
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • If Thread 2 gets lock3 and lock4, then Thread 1 gets lock1 and lock2, the whole thing _will_ deadlock unless Thread 1 releases lock1 and lock2 and retries. This is kind of the whole point of my question. By this interpretation of the spec, which I agree is natural, there is no implementation that does not require a busy loop in general. Busy loops are Very Bad. Conclusion: This gadget should generally be avoided. – Nemo Aug 29 '13 at 22:02
  • 7
    In a quality implementation, such as that shown in http://stackoverflow.com/a/14525010/576911, the "busy loop" isn't busy. After detecting that one of the mutexes is locked, the implementation should unlock everything, and then block **on the locked mutex**. If it instead just blindly tries to lock anything, then that indeed would be expensive, and a poor implementation. – Howard Hinnant Aug 29 '13 at 22:15
  • OK, I understand what you are doing. But if you generalize it to N locks, I am concerned you have actually increased the chances of livelock by "rotating" the effective locking order. Picture many threads (or even just two) encountering a single lock() call over many locks, but deciding to block at different points in the list. I think your implementation could easily result in them "chasing each others' tails" as they run around the list, and I think the likelihood of this grows with the number of locks. It is not at all obvious that your algorithm "has very large eigenvalues" in this case. – Nemo Aug 29 '13 at 23:55
  • 1
    I have thoroughly tested with N == 2, 3, 4. I have not tested with N == 100, 1000, 1000000. I agree you may be correct when N is high enough. I am not concerned about the use case where N is so high, even if it would create the scenario you describe. I have not seen real-world code requiring such a high N. If in the future motivating algorithms are developed that require such a high N, and if testing with quality implementations demonstrates any significant amount of like-lock, then you will be correct, and `std::lock` should be avoided for those use cases. – Howard Hinnant Aug 30 '13 at 00:13
  • I should also stress that the `yield` shown in http://stackoverflow.com/a/14525010/576911 is an important performance optimization. Without the `yield`, a high contention test will demonstrate some amount of live-lock. In no case have I seen the live-lock be permanent. It merely slows things down. Inserting a `yield` just prior to blocking on a mutex which was recently detected to be locked, has been experimentally shown to significantly decrease live-lock and increase system throughput. – Howard Hinnant Aug 30 '13 at 01:05
  • 1
    What happens with two locks and lots of threads? Say 500 threads wind up blocked waiting for `l0` while 500 more are blocked waiting for `l1`? Are you sure that cannot be a stable livelock, as each mutex release on one side makes one thread on the other side runnable, and all 500+500 just trade places? Why do you expect this to resolve itself, and how do you know how long it will take? (Note that this is not a critique of your implementation, which I agree is clever. This is a critique of the entire "back off and retry" design.) – Nemo Aug 30 '13 at 15:33
  • 7
    @Nemo: Do you ever code and test anything yourself? Or do you only do test-less critiques? How about you code up 500 threads, test it, and report back what you find. – Howard Hinnant Aug 30 '13 at 16:39
  • The rot set in with Lockable, and now the poison of polling for locks is spreading. If complex multiple resource management is required, std::lock is just... it sucks. – Martin James May 10 '14 at 19:55
  • 2
    @HowardHinnant The problem with results you get from testing is that you cannot rely on them. The next CPU, operating system, standard library, or compiler version may behave differently. You can, of course, reasonably rely on the platform's standard library being implemented well given the constraints the standard places on it. The problem is that the standard places very bad constraints on this function such that in effect platforms will have to be designed to support it efficiently or it will have to be inefficient. – David Schwartz May 10 '14 at 20:45
  • 3
    @DavidSchwartz: Hi! By coincidence this is the subject I've been working on today. There is now a link in my answer to a more complete treatment of this subject. I freely admit that the tests I have run have been constrained to a single OS. I invite everyone, including yourself, to copy the code out of the paper and report back results from their OS of choice. – Howard Hinnant May 10 '14 at 22:02
  • 1
    @HowardHinnant, «How about you code up 500 threads, test it, and report back what you find» — there's no need in such test in the context of the Nemo's question. Such test would only show that live-lock _usually_ resolves itself in some time _on the modern hardware_. But it won't _prove_ that there're any guarantees. – Sasha Apr 02 '19 at 11:41
  • Very interesting paper, Howard. I just saw a talk yesterday at CppCon2020. https://www.youtube.com/watch?v=lrf5OPGtOoI Code, Slides: http://github.com/BobSteagall/CppCon2020 Dealing with the number of forks in the thousands, the number of philosophers was16, and each philosopher required a random number, maybe a couple of dozen, forks. His algorithm was similar to your "persistent" except that philosophers had time-stamps on them, and marked the forks with time-stamps. I'm concerned that anything other than "ordered" could starve a philosopher who needs a lot of forks. – Bill Chapman Sep 18 '20 at 19:11
4

Your confusion with the standardese seems to be due to this statement

5 Effects: All arguments are locked via a sequence of calls to lock(), try_lock(), or unlock() on each argument.

That does not imply that std::lock will recursively call itself with each argument to the original call.

Objects that satisfy the Lockable concept (§30.2.5.4 [thread.req.lockable.req]) must implement all 3 of those member functions. std::lock will invoke these member functions on each argument, in an unspecified order, to attempt to acquire a lock on all objects, while doing something implementation defined to avoid deadlock.

Your example 3 has a potential for deadlock because you're not issuing a single call to std::lock with all objects that you want to acquire a lock on.

Example 2 will not cause a deadlock, Howard's answer explains why.

Community
  • 1
  • 1
Praetorian
  • 106,671
  • 19
  • 240
  • 328
  • This appears to be the same answer as @paxdiablo's below. Please see my comments there. – Nemo Aug 29 '13 at 21:32
  • 2
    @Nemo Comment 1: the order of arguments to `std::lock` doesn't matter, so both calls will achieve the same thing. The calls cannot deadlock because once Thread 1 acquires lock (which is guaranteed deadlock-free), Thread 2 will wait until Thread 1 has relinquished the locks (and vice versa). Comment 2: The same applies to this case too, regardless of the fact that Thread 2 attempts to lock an additional mutex. – Praetorian Aug 29 '13 at 21:38
  • I am honestly baffled how anyone can get that from the language in the standard. I understand that is what you _want_ it to mean -- as do I, actually -- but it is plainly not what it _says_. – Nemo Aug 29 '13 at 21:49
2

Did C++11 adopt this function from Boost?

If so, Boost's description is instructive (emphasis mine):

Effects: Locks the Lockable objects supplied as arguments in an unspecified and indeterminate order in a way that avoids deadlock. It is safe to call this function concurrently from multiple threads with the same mutexes (or other lockable objects) in different orders without risk of deadlock. If any of the lock() or try_lock() operations on the supplied Lockable objects throws an exception any locks acquired by the function will be released before the function exits.

Jeremy
  • 5,055
  • 1
  • 28
  • 44
  • 2
    This text does not appear in the C++ standard. We can assume that the text in the C++ standard is the intended specification and it may differ from what Boost did. – M.M Apr 30 '15 at 13:03
  • The omission of this text is a significant relaxation of the specification, and brings us back to the essence of the original question: is this relaxation deliberate (which of course we _ought_ to be able to assume), or is it a flaw in the standard which makes the requirement unworkable? – Jeremy Apr 30 '15 at 15:34
  • Well, the Boost version is unworkable (according to OP) whereas the C++ version is OK (according to Howard) because the C++ version does not specify that there is no deadlock if called from multiple threads with the same mutexes in different orders. – M.M Apr 30 '15 at 20:44
  • The OP doesn't mention Boost, so I don't think we can impute to it too many conclusions in that respect. – Jeremy May 19 '15 at 07:55
  • 2
    My point is that the C++ version guarantees that _any_ call to lock(), with _any_ combination of mutexes, is guaranteed _never_ to deadlock, full stop. I pointed out the Boost version because it seems to show that lock() had its genesis in a far more restrictive use case. That's what I understood the OP to to be questioning, and Howard Hinnant (among others) to be defending: whether the underlying implementation required to meet the C++ requirements can result in a sufficiently well-performing outcome in all (reasonable) cases. – Jeremy May 19 '15 at 08:01
  • @Jeremy Exactly. You can meet the boost requirements just by sorting the locks and then calling `lock()` on each one in a canonical order. You cannot meet the C++ standard's requirements that way. – David Schwartz Feb 15 '19 at 16:41