[Pitch] Document (the lack of) spurious failures in `Mutex.withLockIfAvailable(_:)`

The documentation for Mutex.withLockIfAvailable(_:) does not specify if it can spuriously fail to acquire the mutex. Normally, the lack of any discussion about spurious failures wouldn't be an issue since we don't document whether or not most other API can spuriously fail either.

What is a "spurious failure"?

Given the following algorithm:

if mutex.tryLock() {
  doWork()
  mutex.unlock()
}

mutex.tryLock() should return true if the lock was acquired, and false if another thread has currently acquired it. A spurious failure occurs if the function returns false despite no other thread having acquired it.

Why should we document our behavior?

Mutex.withLockIfAvailable(_:) is Swift's spelling of the tryLock() operation, and there is a schism between the C/C++ standards and POSIX as to whether tryLock() might fail spuriously.

C/C++ vs. POSIX

C11's mtx_trylock() and C++11's std::mutex::try_lock() are both allowed to spuriously fail. Per the C++11 standard §30.4.1.1/16:

An implementation may fail to obtain the lock even if it is not held by any other thread. [ Note: This spurious failure is normally uncommon, but allows interesting implementations based on a simple compare and exchange [...] ]

(The standard is referring to weak compare-and-exchange operations which can fail spuriously at the hardware level; strong compare-and-exchange operations cannot fail in this manner.)

But the POSIX standard for pthread_mutex_trylock() makes no such accommodation, instead stating simply:

The function pthread_mutex_trylock() is identical to pthread_mutex_lock() except that if the mutex object referenced by mutex is currently locked (by any thread, including the current thread), the call returns immediately.

Where does Swift stand?

Swift's current documentation is silent on the matter. As a developer who uses the Swift language and its standard library, if I were approaching this documentation in a vacuum, I wouldn't even consider the possibility of a spurious failure. As a rule we don't concern ourselves with "oh it might randomly fail" as something we need to guard against.

But a developer coming from another language will be bringing with knowledge of that language and its standard library. A developer coming to Swift from C or C++ might rightly ask if Mutex.withLockIfAvailable(_:) is safe to use.

I've reviewed the platform-specific implementations of _MutexHandle._tryLock() in the Swift repository and have confirmed that none of these implementations is subject to spurious failure.

Proposed solution

I propose that the documentation for Mutex.withLockIfAvailable(_:) should specifically and clearly indicate whether it can spuriously fail. That way, developers who use Mutex will know what to expect from it and will (hopefully) avoid being confused if they've also read the documentation for the C/C++'s equivalents.

For the full proposal, see here.

8 Likes

What would a programmer be able to do differently if they knew such a call could / could not spuriously fail?

Perhaps not retry? Provided there's a way to distinguish spurious failures from normal failures.

Could even be a runtime check, pseudocode:

if couldFailSpuriously {
    lockWithRetryOnSpuriousFailures(work)
} else {
    lockWithoutRetry(work)
}

func lockWithRetryOnSpuriousFailures(work: () -> Void) -> Bool {
    while true {
        switch tryLock() {
        case .success:
            work()
            unlock()
            return true
        case .normalFailure:
            return false
        case .spuriousFailure:
            break
        }
    }
}

func lockWithoutRetry(work: () -> Void) -> Bool {
    switch tryLock() {
    case .success:
        work()
        unlock()
        return true
    case .normalFailure:
        return false
    }
}

There's some discussion in the pre-pitch thread: Should we document the behavior of `Mutex.withLockIfAvailable()`? - #5 by grynspan

I give an abstract example of the side effects of spurious failures in the full proposal, and as @pyrtsa noted (and as is linked in the proposal) Swift Testing has a real-world need to avoid spurious failures here.

By definition there is no way to distinguish them, because they ultimately come from a low-level operation (weak compare-and-exchange of an atomic value) that represents real and spurious failures the same way.

1 Like

This is a good proposal. I would suggest moving the “Introducing explicit weak and strong variants” item to “future directions” rather than “alternatives considered”.

I'm not opposed to moving it in principle, but in practice there's only a distinction on Linux and Wasm (via a userspace cmpxchg call). So it's at best a hint that we usually ignore. Thoughts?

“future directions” isn’t a commitment: it’s simply something that can happen after the proposal is accepted. “alternatives considered” is more for the things that are foreclosed once the proposal is accepted. It seems to me like introducing a weak variant is in the former category.