0

My Swift code uses objc_sync_enter & objc_sync_exit methods to implement @synchronized primitive (available in Objective-C) in Swift. However, this answer claims it is an outdated as well as inappropriate way to implement synchronised access in Swift. While the reasons for the same are not provided, I would like to know modern ways of implementing critical sections in Swift where a number of variables are accessed in a block, but the same variables are written infrequently (such as when UI orientation or app settings change).

Deepak Sharma
  • 5,577
  • 7
  • 55
  • 131
  • 1
    Have you looked into any sort of locking mechanisms like [`NSLock`](https://developer.apple.com/documentation/foundation/nslock), [`NSRecursiveLock`](https://developer.apple.com/documentation/foundation/nsrecursivelock), [`os_unfair_lock`](https://developer.apple.com/documentation/os/os_unfair_lock) to do locking in Swift? There are resources already here on SO that should be able to help you out. (e.g., you can implement locking with `os_unfair_lock` [like so](https://stackoverflow.com/questions/68614552/swift-access-race-with-os-unfair-lock-lock/68615042#68615042) as an alternative) – Itai Ferber Oct 12 '22 at 13:57
  • 1
    As for _why_ you should avoid `objc_sync_enter`/`objc_sync_exit`, one main reason: it's really easy to get wrong, and lock on a struct, which isn't valid. See, e.g., https://stackoverflow.com/questions/70896707/what-is-the-reason-behind-objc-sync-enter-doesnt-work-well-with-struct-but-wor. (It's also much less performant than other locking mechanisms, but that's a concern in Obj-C too) – Itai Ferber Oct 12 '22 at 14:00
  • (Another similar example of `os_unfair_lock` with some explanation: https://stackoverflow.com/questions/59962747/osspinlock-was-deprecated-in-ios-10-0-use-os-unfair-lock-from-os-lock-h-i/66525671#66525671) – Itai Ferber Oct 12 '22 at 14:03
  • @ItaiFerber Does NSLock and the likes raise a trap in kernel, or are they implemented as a pthread mutex at thread level? – Deepak Sharma Oct 12 '22 at 15:06
  • 1
    `NSLock` uses pthreads; from [the docs](https://developer.apple.com/documentation/foundation/nslock#Overview): "The NSLock class uses POSIX threads to implement its locking behavior". `os_unfair_lock` is implemented entirely differently, but shares the same general ideas. In general, though, I wouldn't be concerned about the specific implementation details in either case: just know that these are modern tools that are safe to use from Swift. – Itai Ferber Oct 12 '22 at 16:02
  • 2
    My rule of thumb about locks recently is “if you have to ask, you shouldn’t use them”. Try the more structured synchronization/concurrency primitives first (Actors, Dispatch Queues), and only drop down to manual twiddling with locks if you have a very compelling reason – Alexander Oct 12 '22 at 17:20
  • 1
    @ItaiFerber “`NSLock` ... `os_unfair_lock` ... in either case: just know that these are modern tools that are safe to use from Swift.” I know you know this, but one has be extremely careful with `os_unfair_lock` from Swift. This is why iOS 16 introduced [`OSAllocatedUnfairLock`](https://developer.apple.com/documentation/os/osallocatedunfairlock), to avoid the [rigmarole](https://stackoverflow.com/questions/59962747/66525671#66525671) required to use `os_unfair_lock` safely from Swift. Better to stick w actors or GCD, IMHO, unless performance is of paramount concern (which it generally isn’t). – Rob Oct 16 '22 at 21:18
  • 1
    @Rob Absolutely! I'm glad you brought up more modern tools because I was thinking much too narrowly. I actually wasn't aware of `OSAllocatedUnfairLock`, so that's great to know. Thanks for your insight as always! – Itai Ferber Oct 16 '22 at 23:39
  • I watched the wwdc video and read couple of articles on Swift Actors. However, they are not like locks. They require all variables to be embedded inside Actor and guarantee synchronous access to all properties inside Actor. That is a big constraint, and not as flexible as locks/DispatchQueues. I do not want to embed all properties in one Actor. That would require major redesign of the code. – Deepak Sharma Nov 05 '22 at 18:38
  • I understand that actors might seem like a lot when you first encounter them, but it is a very elegant and natural mechanism when using `async`-`await`. It is the “modern” mechanism for codebases that use the new Swift concurrency. Now, if you're not there yet, and you are trying to quickly retrofit something in lieu of `objc_sync_enter` & `objc_sync_exit`, then GCD and/or locks are a reasonable alternative, requiring little code refactoring. – Rob Nov 08 '22 at 19:25

1 Answers1

0

You asked:

I would like to know modern ways of implementing critical sections in Swift where a number of variables are accessed in a block

The modern way is to use actors. See The Swift Programming Guide: Concurrency: Actors:

actor TemperatureLogger {
    let label: String
    var measurements: [Int]
    private(set) var max: Int

    init(label: String, measurement: Int) {
        self.label = label
        self.measurements = [measurement]
        self.max = measurement
    }

    ...

    func update(with measurement: Int) {
        measurements.append(measurement)
        if measurement > max {
            max = measurement
        }
    }
}

Also see WWDC 2022 video Eliminate data races using Swift Concurrency or 2021’s Protect mutable state with Swift actors.


In codebases where you have not yet adopted Swift concurrency (e.g., async-await) you could use a GCD serial queue:

class TemperatureLogger {
    let label: String

    private var _measurements: [Int]                           // private backing variable
    var measurements: [Int] { synchronized { _measurements } } // synchronized computed var to return value

    private var _max: Int                                      // private backing variable
    var max: Int { synchronized { _max } }                     // synchronized computed var to return value

    init(label: String, measurement: Int) {
        self.label = label
        self._measurements = [measurement]
        self._max = measurement
    }

    func update(with measurement: Int) {
        synchronized {
            _measurements.append(measurement)
            if measurement > _max {
                _max = measurement
            }
        }
    }

    private let queue = DispatchQueue(label: "TemperatureLogger")

    private func synchronized<T>(block: () throws -> T) rethrows -> T {
        try queue.sync {
            try block()
        }
    }
}

Serial queues are a simple, yet robust, synchronization mechanism. Just make sure you perform all interactions (both reads and writes) through this queue. Specifically, do not expose the private backing variable, but rather only provide mechanisms (such as computed variable, update methods, etc.) that ensure the state is properly synchronized.

Now, no discussion of GCD synchronization mechanisms can pass without a nod to the “reader-writer” pattern (in which one uses concurrent GCD queue, performing writes asynchronously with a barrier and performing reads synchronously without a barrier). While this is intuitively appealing, it actually offers only a modest performance improvement but introduces all sorts of complicating factors, especially in thread-explosion scenarios. Rather than the reader-writer pattern, if performance were really critical, one might use a lock, which ends up being even more performant:

private let lock = NSLock()

private func synchronized<T>(block: () throws -> T) rethrows -> T {
    lock.lock()
    defer { lock.unlock() }
    return try block()
}

But in the vast majority of cases, this would be premature optimization. Only fall back to low-level mechanisms, such as locks, where performance is paramount. And be especially careful if contemplating unfair locks from Swift.

See https://stackoverflow.com/a/73314198/1271826 and the links contained therein for a discussion of a variety of synchronization mechanisms.

But in short, actors are the modern solution, GCD are the robust, older solution, and locks are useful in edge-cases where performance is critical, but must be used carefully. But objc_sync_enter and objc_sync_exit are of little-to-no practical use anymore.


You go on to say:

... where a number of variables are accessed in a block, but the same variables are written infrequently (such as when UI orientation or app settings change).

If it is simply for UI updates, a common technique is merely to dispatch these blocks to the main queue. This works because (a) all UI updates must happen on the main thread, anyway; and (b) the main queue is a serial queue.

In short, especially when for UI updates, dispatching related model and UI updates the main queue is a quick-and-dirty synchronization mechanism.

Rob
  • 415,655
  • 72
  • 787
  • 1,044