2

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?

3 Answers3

5

According to the WWDC 2016 talk "Concurrent Programming with GCD in Swift 3" at around 18:07 the speaker states that

Traditionally you would use a lock. And in Swift, since you have the entire Darwin module at your disposition, you will actually see the struct based traditional C locks. However [emphasis added], Swift assumes that anything that's a struct can be moved, and that doesn't work with a mutex or with a lock.

The solution is to bridge to Objective-C and create a class which wraps the os_unfair_lock as an ivar:

And if you want something that's smaller and that looks like the locks that you have in C, then you have to call into Objective-C and introduce a base class in Objective-C that has your lock as an ivar

In this case, something like

UnfairLock.h

#ifndef UnfairLock_h
#define UnfairLock_h

@import Foundation;
@import os;

@interface UnfairLock : NSObject

-(void)unfairlyAcquire;
-(void)unlock;

@end


#endif /* UnfairLock_h */

UnfairLock.m

#import <Foundation/Foundation.h>
#import "UnfairLock.h"

@implementation UnfairLock {
    os_unfair_lock _lock;
}

-(instancetype)init {
    self = [super init];
    
    if (self) {
        _lock = OS_UNFAIR_LOCK_INIT;
    }
    
    return self;
}


-(void)unfairlyAcquire {
    os_unfair_lock_lock(&_lock);
}

-(void)unlock {
    os_unfair_lock_unlock(&_lock);
}

@end
5

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:

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
    }
}
Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
0

beat this

import os.lock

class Locker {

  func lock() {
    os_unfair_lock_lock(oslock)
  }

  func unlock() {
    os_unfair_lock_unlock(oslock)
  }

  func trylock() -> Bool {
    return os_unfair_lock_trylock(oslock)
  }

  let oslock = {
    let lock1 = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
    lock1.initialize(to: .init())
    return lock1
  }()

  deinit {
    oslock.deinitialize(count: 1)
    oslock.deallocate()
  }
}
// property
let lock = Locker()

func example() {
  lock.lock()
    /* critical code */
  lock.unlock()
}

can even put Locker in arrays and dictionaries.

D Lindsey
  • 111
  • 6