The documentation for Mutex.withLockIfAvailable() does not specify if it can spuriously fail to acquire the mutex. I'd like to get feedback from the community: should we make clear that it cannot fail in this fashion?
This isn't a formal Swift Evolution proposal, but I'd like our documentation to state that Mutex.withLockIfAvailable() will not spuriously fail if its underlying platform-specific primitive is not held by another thread. Specifically, I'd like to strengthen the language in the Return Value documentation section:
- The return value, if any, of the `body` closure parameter or `nil` if the lock
- couldn’t be acquired.
+ If the lock is acquired, the return value of the `body` closure. If the lock
+ is already held by any thread (including the current thread), `nil`.
And I would like to add the following Note callout to the Discussion section:
+ - Note: This function cannot spuriously fail to acquire the lock. The behavior
+ of similar functions in other languages (such as C's `mtx_trylock()`) is
+ platform-dependent and may differ from Swift's behavior.
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 bother?
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. However, 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 topthread_mutex_lock()except that if the mutex object referenced bymutexis currently locked (by any thread, including the current thread), the call returns immediately.
Other languages
Other languages have equivalent API: Rust has mutex::try_lock(), Go has Mutex.TryLock(), Kotlin has Mutex.tryLock(), and Java has Lock.tryLock(). None of these methods document that they spuriously fail (other than the Rust-specific concept of a "poisoned" mutex which is not germane to this pitch.) So that may be an argument in favour of us leaving the documentation as-is.
Where does Swift stand?
It's my position that Swift should document the behaviour specifically. 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.
I've gone over the platform-specific implementations of _MutexHandle._tryLock() in the Swift repository and have confirmed that none of these implementations is subject to spurious failure (or, at least, none documents any such failure mode):
| Platform | Implementation Based On | Uses cmpxchg? |
Documents Possible Spurious Failures? |
|---|---|---|---|
| Darwin | os_unfair_lock_trylock() |
No | No |
| FreeBSD | UMTX_OP_MUTEX_TRYLOCK |
No | No |
| Linux/Android | Atomic.compareExchange()/FUTEX_TRYLOCK_PI |
Strong | No |
| OpenBSD | pthread_mutex_trylock() |
No | No |
| Wasm | Atomic.compareExchange() |
Strong | No |
| Windows | TryAcquireSRWLockExclusive() |
No | No |
In other words, although we don't document it, Swift's withLockIfAvailable() implementations do not spuriously fail. These implementations are, of course, implementation details and are subject to change over time, but any change to Mutex that causes us to start spuriously failing is a breaking change because it could wreak havoc on code that uses withLockIfAvailable() today.
Or maybe I'm just overthinking it. What do you all think? ![]()