Doc. No.: | WG21/N3209 J16/10-0199 |
---|---|
Date: | 2010-11-11 |
Reply to: | Hans-J. Boehm, Pablo Halpern |
Phone: | +1-650-857-3406 |
Email: | Hans.Boehm@hp.com, phalpern@halpernwightsoftware.com |
National body comment US 3 points out that the FCD makes no meaningful progress guarantees for multithreaded C++ programs. US 186 points out that the current try_lock specification allows the function to always spuriously fail, again making it impossible for programmers to provide useful progress guarantees.
The complaints are clearly correct; we say nothing about progress guarantees. Many real applications will need to make some assumptions beyond the standard. The question is whether we want to address this directly, or view it in the same way that we currently view performance guarantees: as a quality of implementation issue. The correct answer is not clear. Here we argue for a fairly conservative solution that adds at most "normative encouragement" to the specification. The arguments in favor of such a minimalist solution are essentially:
Our proposal implements the "normative encouragement" consensus that appeared in Rapperswil, though one of us is mildly concerned that even this may be stronger than appropriate.
Here we outline the difficulties in doing anything more substantial.
Thread 1 | Thread 2 |
x = true; print "hello"; ... | while (!x) {} |
Lock implementations sometimes use a briefly held spin lock to protect the internal data structures of the lock. Any real notion of fairness seems unattainable if an unlucky thread can repeatedly fail to acquire this spin lock, as appears likely in such a setting.
Thus imposing an absolute requirement here is tricky, and may be inappropriate for some settings. The normative encouragement option suggested by US16 as a possible alternative seems much more palatable.
If we choose not to provide hard progress guarantees for the C++ threads in general, there are fewer reasons to say anything about try_lock() spurious failures.
It can still be argued that some legitimate algorithms could rely on the fact that try_lock() failed to obtain useful failures. One example, due to the authors of US 186, is that a graph traversal algorithm might legitimately conclude that if a lock on a node is held by another traversal thread, this node is already being visited by another thread, and hence no further work needs to be done.
However, it appears to us that such algorithms can almost always be more simply rewritten using the atomics library. A graph traversal algorithm typically already needs a flag to indicate whether a node has been visited. If that flag is made atomic, rather than protected by a lock, it can also easily be used to detect an in-progress traversal and ensure that exactly one thread processes each node, if that is desired.
On the other hand, there are strong reasons to require that programs be written to tolerate spurious try_lock() failures:
(All of these were brought out in the reflector discussion that also included Herb Sutter, Bronek Kozicki, Lawrence Crowl.)
On the other hand, we would like to discourage implementations from generating actual spurious failures of try_lock() when they could be avoided, since those introduce performance problems. And we want to make it clear that a try_lock() implementation that always fails is not useful. Thus we propose either normative encouragement, or a note, to explicitly discourage such implementations.
Implementations should ensure that all unblocked threads eventually make forward progress. [Note: Standard library functions may silently block on I/O or locks. Factors in the execution environment, including externally-imposed thread priorities, may prevent an implementation from making certain guarantees of forward progress. -- end note]We also propose to add at the end of 1.10 [intro.multithread]:
An implementation should ensure that the last value (in modification order) assigned by an atomic or synchronization operation will become visible to all other threads in a finite period of time.Recall that it is currently unclear whether existing hardware easily supports this as a hard guarantee. But a lot of existing software would not work well if the guarantee were frequently violated. (Note that the corresponding property for ordinary objects is not observable without introducing a data race.)
And finally add at the end of 30.4.1p14 [thread.mutex.requirements]
An implementation should ensure that try_lock() does not consistently return false in the absence of contending mutex acquisitions.
(Does "should" make sense in a requirements table like this? I think it is OK to apply to user-defined implementations, as well as the built-in mutex types. It will in fact apply to only the built-in mutex types if the lockable requirements paper goes in, to both if it doesn't. This is is too vague and untestable to be a "shall" requirement.)
For consistency, add to the compare_exchange_weak specification [atomics.types.operations] 29.6p23, just before the note:
Implementations should ensure that weak compare-and-exchange operations do not consistently return false unless either the atomic object has value different from expected, or there are concurrent modifications to the atomic object.
By stating these explicitly, we preserve our options, but potentially avoid some misunderstandings.