Catfish-Man
@Catfish-Man

A decade or so ago, "what lock should I use?" was a fairly complicated question on macOS/iOS. We had:

  • pthread_mutex_t: 64 bytes, FIFO12, weird API, pretty slow when uncontended
  • os_spinlock: 4 bytes, unfair, super fast when uncontended, burns power and CPU when contended
  • dispatch_queue_t + dispatch_sync: 80ish bytes, FIFO, a bit faster than pthread_mutex_t, bonus feature if you want to do async stuff
  • @synchronized(…): slow, heavy, FIFO, always recursive (not actually a good thing), exception-safe (less useful than it sounds)
  • NSLock: a pthread_mutex_t with extra overhead
  • dispatch_semaphore_t misused as a lock: mid-sized, unfair, pretty fast when uncontended, actually surprisingly appealing

Then two important things happened. One was that we started relying WAY more on thread priorities, and all the locks that can't donate priority (spinlocks, semaphores) suddenly became essentially unusable. The other was we introduced a new kind of lock:

✨os_unfair_lock✨

4 bytes, unfair, super fast uncontended, efficient when contended, donates priority, enforces correct use via assertions. I've had so many conversations like this:

“hey what lock should I--”

“unfair lock”

“don’t you need more info about the context?”

“honestly not really”

(The one glaring downside is that due to language issues in Swift it's less efficient and requires more shenanigans to use than it does in C/ObjC/C++, but OSAllocatedUnfairLock mostly fixes this in recent systems if you can't use the built-in concurrency constructs)

UPDATE FROM THE FUTURE: The type introduced in This Swift Evolution proposal fixes3 the remaining issues here, getting performance parity with using os_unfair_lock from C/ObjC. It's also cross platform :)


  1. FIFO sounds like an appealing property in a lock, "sure, the first thread to wait should be the first that gets to go", but in practice it's rarely actually useful, and the performance cost under contention can be huge due to requiring repeatedly switching threads and letting lower priority threads go before higher priority ones.

  2. pthread mutexes on Darwin (and things that use them) were FIFO in the early 2010s when the dilemma being described was current, but are not in more recent OS versions

  3. well, will fix, once it's accepted



You must log in to comment.

in reply to @Catfish-Man's post:

…we started relying WAY more on thread priorities, and all the locks that can't donate priority (spinlocks, semaphores) suddenly became essentially unusable.

Foundation[1] got so many referrals of perf/hang bugs around this time. Usually because someone chose dispatch_semaphore previously (often years previously), and the knight from “Indiana Jones and the Last Crusade” had just shown up to inform them “you chose… poorly”. This took the form of a hang in their process in a thread (or sometimes many threads) in dispatch_semaphore_wait, waiting on a thread that was calling out to Foundation (hence sending the bug to us).

The threads that were hung had higher priority than the thread that was being waited on, but they couldn't donate their priority to the latter because semaphores don't know where to donate priority to — the semaphore doesn't know what thread will unblock it until that thread signals it, which happens after whatever work is holding everything up.

After some internal mailing list discussions educated some of us on the importance of treating dispatch_semaphore as if it were a swarm of bees, I came to regard seeing one or more threads hung in dispatch_semaphore_wait as, if not already sufficient grounds to send the bug back, at least a strong smell.

[1]: For anyone reading this who doesn't know: I was the bug screener for Foundation and Core Foundation from 2014 to 2018.

Nah. For every hang in one of your components or in other code in Foundation, there were at least four or five elsewhere.

I threw back roughly 80–90% of bugs sent our way generally. You saw the ones I didn't (or that got sent back to me with a more convincing case for it actually being a Foundation bug).

One was that we started relying WAY more on thread priorities, and all the locks that can't donate priority (spinlocks, semaphores) suddenly became essentially unusable.

And reader/writer locks. (Can't quickly record all the readers that a waiting writer might need to donate its priority to.) I rebuilt the Objective-C runtime's internal locking around rwlocks, and then they were taken away from me 😢