swiftlockingdata-racethread-sanitizer

Swift access race with os_unfair_lock_lock


I made a custom property wrapper which provides a method to access data in a mutually exclusive context using an os_unfair_lock. After testing my wrapper with TSAN enabled, an access race error was reported at the point of lock acquisition using os_unfair_lock_lock (shown in the image below)

Access race image

Somehow a locking structure which is supposedly thread-safe is reported by TSAN as not being so. What is going on here?


Solution

  • Edit: As of iOS 16+/macOS 13+/etc., you no longer need to (and should not) do the below: OSAllocatedUnfairLock is the official and preferred interface for os_unfair_lock in Swift.

    Better yet, if you can target iOS 18+/macOS 15+, the official Mutex type exposes an OS-agnostic lock (wrapping os_unfair_lock on Darwin platforms under the hood) while avoiding the allocation altogether.


    One alternative (and possibly more direct) approach to your self-answer is to heap-allocate the lock in Swift directly, as opposed to bridging over to Objective-C to do it. The Objective-C approach avoids the issue by calling the lock functions from a different language, with different semantics — C and Objective-C don't move or tombstone value types passed in to functions by inout reference; but you can also avoid the problem in pure Swift, by not taking an inout reference at all:

    let lock = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
    lock.initialize(to: .init())
    
    // later:
    
    os_unfair_lock_lock(lock)
    defer { os_unfair_lock_unlock(lock) }
    

    Heap-allocating allows you to pass a pointer directly into the function, and pointers are reference types in Swift — while Swift can move the pointer value itself, the memory it references will remain untouched (and valid).

    If you go this route, don't forget to deinitialize and deallocate the memory when you want to tear down the lock:

    lock.deinitialize(count: 1)
    lock.deallocate()
    

    If you'd like, you can create a similar UnfairLock interface in Swift, including functionality like your own mutexExecute: emphasized text typealias UnfairLock = UnsafeMutablePointer<os_unfair_lock>

    extension UnfairLock {
        static func createLock() -> UnfairLock {
            let l = UnfairLock.allocate(capacity: 1)
            l.initialize(to: .init())
            return l
        }
    
        static func destructLock(_ lock: UnfairLock) {
            lock.deinitialize(count: 1)
            lock.deallocate()
        }
    
        func whileLocked<T>(_ action: () throws -> T) rethrows -> T {
            os_unfair_lock_lock(self)
            defer { os_unfair_lock_unlock(self) }
            return try action()
        }
    }
    

    Usage:

    init() {
        lock = UnfairLock.createLock()
    }
    
    deinit {
        UnfairLock.destructLock(lock)
    }
    
    func performThing() -> Foo {
        return lock.whileLocked {
            // some operation that returns a Foo
        }
    }