Speaking for myself (i.e. not as the review manager), the proposal makes a reasonable case for this change, but I believe there's an even stronger argument for it. In particular, I think the C and C++ committees were wrong to standardize a weak tryLock operation.
The only reason to specify a weak tryLock is to let it be implemented with a weak compare-exchange (CE). When both are directly implemented in hardware with similar ordering requirements, weak CEs are not generally faster than strong CEs; the theoretical benefit of asking for a weak CE is just this:
- on architectures where strong CEs must be emulated with weak CEs, and therefore implementing a strong CE requires a loop,[1]
- given that a CE generally already needs to be in a loop in order to implement the transactional semantics of whatever operation it's part of,
- a weak CE avoids the nested loop that would be necessary if a strong CE were used instead.
Using a weak CE is therefore a pretty minor and architecture-specific optimization to begin with, which is why the common advice (including Raymond Chen's, as cited in the proposal) is to use it sparingly and only when the retry operation of your transaction loop is cheap enough that it's better to just do it than to bother checking for spurious failure.
This makes the concept of a weak tryLock pretty weird. A programmer using tryLock is, presumably, doing so as part of a transactional operation of some sort. A transactional operation should generally not fail spuriously, so the code must eventually try again if tryLock spuriously fails. So far, this is similar to the situation with weak and strong CEs: there must be a loop that repeatedly tries to lock, and it might be a nice optimization to use a weak tryLock if the retry step of this loop is cheap. If the retry step is expensive, of course, it'd be better to repeat the lock attempt until it either succeeds or fails non-spuriously — which is to say, it'd be better to use a strong tryLock.
But the tryLock APIs standardized by C and C++ do not distinguish between failure modes! They just return false on any kind of failure, and since the atomic state of the mutex is encapsulated, there's no way for the caller to determine whether failure was spurious or not. Any operation that tries again for spurious failure must therefore also try again for non-spurious failure. If our transactional loop does this by just calling tryLock again, then it's just repeatedly calling tryLock until the mutex actually becomes available, which means we've degraded our mutex to a spin lock. So either we're misusing the mutex or the operation is doing something weirder than just repeatedly calling tryLock, like mixing the attempts with sleeps or lock calls. Our effort to find an operation that's justified in calling a weak tryLock is getting increasingly hairy.
Now, I can think of operations that do mix up lock attempts like that. For example, there are deadlock-free algorithms for acquiring multiple locks that are based on locking one of the locks, tryLocking the rest, and backing off when one of the latter attempts fails. However, that backoff step is very expensive, which is not the situation where we want to be using a weak CE and risking spurious failure. At best, we may want to optimistically use weak tryLock when probing locks, but we'd still want to use a strong tryLock to verify that a lock is actually taken before giving up. And you'd probably want to know immediately whether a weak tryLock failed for spurious or non-spurious reasons.
Meanwhile, a strong tryLock is not just a nice optimization when the retry step is expensive. A strong tryLock tells you that at least one other thread has tried to acquire the mutex, which is a useful piece of information. In some algorithms, that might be good enough to avoid acquiring the mutex yourself.
In summary:
- Strong
tryLockis useful in operations where the retry step is either unnecessary or expensive enough to want to avoid doing spuriously. This covers most operations that are usingtryLock. - Many architectures gain no theoretical benefit from a weak
tryLockbecause they provide a strong CE. - I can imagine legitimate uses for a weak
tryLock, but only as a minor optimization that would be backed by calling a strongtryLock, and I'm skeptical that it would matter for the sorts of algorithms that would try it. - The uses I can imagine for a weak
tryLockwould all benefit from knowing whether failure was spurious or non-spurious, which the C and C++ APIs don't tell you.
My conclusion is that mutex APIs should always offer a strong tryLock. If they're going to also provide a weak tryLock (which I do not feel is sufficiently justified at this time), it should distinguish spurious and non-spurious failures to the caller. And the default tryLock that programmers reach for should certainly be a strong rather than a weak one. So the C and C++ APIs, which offer only a weak tryLock that does not distinguish failure modes, are misdesigned in multiple ways.
Applying this to Swift, Mutex.withLockIfAvailable(_:) should just perform a strong tryLock. That is exactly what is suggested by this proposal. If we ever find an important reason to add a weak tryLock, we can do that as a future refinement, with careful attention provided to the need to distinguish failure modes.
On architectures like this, strong CEs are implemented by repeatedly performing a weak CE until it either succeeds or fails for non-spurious reasons. A failed CE still refreshes the current value of the atomic, so if your weak CE fails, you can easily check whether it failed spuriously by just comparing the new current value with the value you were trying to compare against. ↩︎