32

I need a read\write lock for my application. I've read https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock

and wrote my own class, cause there are no read/write lock in swift

class ReadWriteLock {

    var logging = true
    var b = 0
    let r = "vdsbsdbs" // string1 for locking
    let g = "VSDBVSDBSDBNSDN" // string2 for locking

    func waitAndStartWriting() {
        log("wait Writing")
        objc_sync_enter(g)
        log("enter writing")
    }


    func finishWriting() {
        objc_sync_exit(g)
        log("exit writing")
    }

    // ждет пока все чтение завершится чтобы начать чтение
    // и захватить мютекс
    func waitAndStartReading() {

        log("wait reading")
        objc_sync_enter(r)
        log("enter reading")
        b++
        if b == 1 {
            objc_sync_enter(g)
            log("read lock writing")
        }

        print("b = \(b)")
        objc_sync_exit(r)
    }


    func finishReading() {

        objc_sync_enter(r)

        b--

        if b == 0 {
            objc_sync_exit(g)
            log("read unlock writing")
        }

        print("b = \(b)")
        objc_sync_exit(r)
    }

    private func log(s: String) {
        if logging {
            print(s)
        }
    }
}

It works good, until i try to use it from GCD threads.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

When i try to use this class from different async blocks at some moment it allows to write when write is locked

here is sample log:

wait reading
enter reading
read lock writing
b = 1
wait reading
enter reading
b = 2
wait reading
enter reading
b = 3
wait reading
enter reading
b = 4
wait reading
enter reading
b = 5
wait reading
enter reading
b = 6
wait reading
enter reading
b = 7
wait reading
enter reading
b = 8
wait reading
enter reading
b = 9
b = 8
b = 7
b = 6
b = 5
wait Writing
enter writing
exit writing
wait Writing
enter writing 

So, as you can see g was locked, but objc_sync_enter(g) allows to continue. Why could this happen ?

BTW i checked how many times ReadWriteLock constructed, and it's 1.

Why objc_sync_exit not working and allowing to objc_sync_enter(g) when it's not freed ?

PS Readwirtelock defined as

class UserData {

    static let lock = ReadWriteLock()

Thanks.

Lewis Hai
  • 1,114
  • 10
  • 22
Yegor Razumovsky
  • 902
  • 2
  • 9
  • 26

5 Answers5

58

objc_sync_enter is an extremely low-level primitive, and isn't intended to be used directly. It's an implementation detail of the old @synchronized system in ObjC. Even that is extremely out-dated and should generally be avoided.

Synchronized access in Cocoa is best achieved with GCD queues. For example, this is a common approach that achieves a reader/writer lock (concurrent reading, exclusive writing).

public class UserData {
    private let myPropertyQueue = dispatch_queue_create("com.example.mygreatapp.property", DISPATCH_QUEUE_CONCURRENT)

    private var _myProperty = "" // Backing storage
    public var myProperty: String {
        get {
            var result = ""
            dispatch_sync(myPropertyQueue) {
                result = self._myProperty
            }
            return result
        }

        set {
            dispatch_barrier_async(myPropertyQueue) {
                self._myProperty = newValue
            }
        }
    }
}

All your concurrent properties can share a single queue, or you can give each property its own queue. It depends on how much contention you expect (a writer will lock the entire queue).

The "barrier" in "dispatch_barrier_async" means that it is the only thing allowed to run on the queue at that time, so all previous reads will have completed, and all future reads will be prevented until it completes. This scheme means that you can have as many concurrent readers as you want without starving writers (since writers will always be serviced), and writes are never blocking. On reads are blocking, and only if there is actual contention. In the normal, uncontested case, this is extremely very fast.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 4
    This is the only answer to the question. `objc_sync_xxx` simply does not work from a GCD thread. I haven't found a good answer as to why, but `objc_sync_enter` doesn't block when it should. Instead, switch to GCD which offers a much more fully featured system anyway. Good tutorial here: https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1 – HughHughTeotl Jul 28 '16 at 08:41
  • Is this technique safe if multiple readers are actually writing properties of `myProperty`? For example, if `myProperty` is an instance of a class, `myProperty.height = 12` calls `get`, but not `Set`. What happens if multiple threads try to set `myProperty.height`? – ConfusedByCode Mar 20 '17 at 20:58
  • The writes will be sequential. That is the point of `dispatch_barrier_sync`. Reads may in interspersed with the writes, but will be consistent and thread-safe. – Rob Napier Mar 21 '17 at 06:42
  • I understand that's the point of `dispatch_barrier_async`, but what if `dispatch_barrier_async` is never called, because setting a property of a class instance doesn't call `set`? `set` is only called when you assign a new object, but not when a property is set. – ConfusedByCode Mar 21 '17 at 14:30
  • If myProperty is a struct, then it is assigned when you modify it. If it's a class then it is responsible for its own thread safety. – Rob Napier Mar 24 '17 at 18:25
  • 1
    Claiming that `@synchronized` is outdated is a strong claim, considering that Apple uses it all over the place in their system APIs even in Big Sur, even in APIs that got essential in Big Sur and are performance critical, like their Network Extension API which you must use in Big Sur as kernel extensions are no longer support for networking extensions. – Mecki Apr 26 '21 at 22:51
23

Are you 100% sure your blocks are actually executing on different threads?

objc_sync_enter() / objc_sync_exit() are guarding you only from object being accessed from different threads. They use a recursive mutex under the hood, so they won't either deadlock or prevent you from repeatedly accessing object from the same thread.

So if you lock in one async block and unlock in another one, the third block executed in-between can have access to the guarded object.

Alex Skalozub
  • 2,511
  • 16
  • 15
13

This is one of those very subtle nuances that is easy to miss.

Locks in Swift

You have to really careful what you use as a Lock. In Swift, String is a struct, meaning it's pass-by-value.

Whenever you call objc_sync_enter(g), you are not giving it g, but a copy of g. So each thread is essentially creating its own lock, which in effect, is like having no locking at all.

Use NSObject

Instead of using a String or Int, use a plain NSObject.

let lock = NSObject()

func waitAndStartWriting() {
    log("wait Writing")
    objc_sync_enter(lock)
    log("enter writing")
}


func finishWriting() {
    objc_sync_exit(lock)
    log("exit writing")
}

That should take care of it!

Sir Wellington
  • 576
  • 1
  • 7
  • 10
2

In addition to @rob-napier's solution. I've updated this to Swift 5.1, added generic typing and a couple of convenient append methods. Note that only methods that access resultArray via get/set or append are thread safe, so I added a concurrent append also for my practical use case where the result data is updated over many result calls from instances of Operation.

public class ConcurrentResultData<E> {

    private let resultPropertyQueue = dispatch_queue_concurrent_t.init(label: UUID().uuidString)
    private var _resultArray = [E]() // Backing storage

    public var resultArray:  [E] {
        get {
            var result = [E]()
            resultPropertyQueue.sync {
                result = self._resultArray
            }
            return result
        }
        set {
            resultPropertyQueue.async(group: nil, qos: .default, flags: .barrier) {
                self._resultArray = newValue
            }
        }
    }

    public func append(element : E) {
        resultPropertyQueue.async(group: nil, qos: .default, flags: .barrier) {
            self._resultArray.append(element)
        }
    }

    public func appendAll(array : [E]) {
        resultPropertyQueue.async(group: nil, qos: .default, flags: .barrier) {
            self._resultArray.append(contentsOf: array)
        }
    }

}

For an example running in a playground add this

//MARK:- helpers
var count:Int = 0
let numberOfOperations = 50

func operationCompleted(d:ConcurrentResultData<Dictionary<AnyHashable, AnyObject>>) {
    if count + 1 < numberOfOperations {
        count += 1
    }
    else {
        print("All operations complete \(d.resultArray.count)")
        print(d.resultArray)
    }
}

func runOperationAndAddResult(queue:OperationQueue, result:ConcurrentResultData<Dictionary<AnyHashable, AnyObject>> ) {
    queue.addOperation {
        let id = UUID().uuidString
        print("\(id) running")
        let delay:Int = Int(arc4random_uniform(2) + 1)
        for _ in 0..<delay {
            sleep(1)
        }
        let dict:[Dictionary<AnyHashable, AnyObject>] = [[ "uuid" : NSString(string: id), "delay" : NSString(string:"\(delay)") ]]
        result.appendAll(array:dict)
        DispatchQueue.main.async {
            print("\(id) complete")
            operationCompleted(d:result)
        }
    }
}

let q = OperationQueue()
let d = ConcurrentResultData<Dictionary<AnyHashable, AnyObject>>()
for _ in 0..<10 {
    runOperationAndAddResult(queue: q, result: d)
}
Rocket Garden
  • 1,166
  • 11
  • 26
1

I had the same problem using queues in background. The synchronization is not working all the time in queues with "background" (low) priority.

One fix I found was to use semaphores instead of "obj_sync":

static private var syncSemaphores: [String: DispatchSemaphore] = [:]

    static func synced(_ lock: String, closure: () -> ()) {

        //get the semaphore or create it
        var semaphore = syncSemaphores[lock]
        if semaphore == nil {
            semaphore = DispatchSemaphore(value: 1)
            syncSemaphores[lock] = semaphore
        }

        //lock semaphore
        semaphore!.wait()

        //execute closure
        closure()

        //unlock semaphore
        semaphore!.signal()
    }

The function idea comes from What is the Swift equivalent to Objective-C's "@synchronized"?, an answer of @bryan-mclemore.

J.S.R - Silicornio
  • 1,111
  • 13
  • 16