Create thread safe array in Swift

IosArraysSwiftMultithreadingRead Write

Ios Problem Overview


I have a threading problem in Swift. I have an array with some objects in it. Over a delegate the class gets new objects about every second. After that I have to check if the objects are already in the array, so I have to update the object, otherwise I have to delete / add the new object.

If I add a new object I have to fetch some data over the network first. This is handelt via a block.

Now my problem is, how to I synchronic this tasks?

I have tried a dispatch_semaphore, but this one blocks the UI, until the block is finished.

I have also tried a simple bool variable, which checks if the block is currently executed and skips the compare method meanwhile.

But both methods are not ideal.

What's the best way to manage the array, I don't wanna have duplicate data in the array.

Ios Solutions


Solution 1 - Ios

Update for Swift

The recommended pattern for thread-safe access is using dispatch barrier:

let queue = DispatchQueue(label: "thread-safe-obj", attributes: .concurrent)

// write
queue.async(flags: .barrier) {
    // perform writes on data
}

// read
var value: ValueType!
queue.sync {
    // perform read and assign value
}
return value

Solution 2 - Ios

I don't know why people take such complex approaches to such a simple thing

  • Don't abuse DispatchQueues for locking. Using queue.sync is nothing more than acquiring a lock and dispatching work to another thread while the lock (DispatchGroup) waits. It is not just unnecessary, but also can have side effects depending on what you are locking. You can look it up yourself in the GCD Source.

    Also GCD does not mix well with the new structured concurrency APIs!

  • Don't use objc_sync_enter/exit, those are used by ObjCs @synchronized which will implicitly bridge Swift collections to a ObjC counterpart, which is also unnecessary. And it is a legacy API.

Just define a lock, and guard your collection access.

var lock = NSLock()
var a = [1, 2, 3]

lock.lock()
a.append(4)
lock.unlock()

If you want to make your life a bit easier, define a small extension.

extension NSLock {

    @discardableResult
    func with<T>(_ block: () throws -> T) rethrows -> T {
        lock()
        defer { unlock() }
        return try block()
    }
}

let lock = NSLock()
var a = [1, 2, 3]

lock.with { a.append(4) }

You can also define a @propertyWrapper to make your member vars atomic.

@propertyWrapper
struct Atomic<Value> {

    private let lock = NSLock()
    private var value: Value

    init(default: Value) {
        self.value = `default`
    }

    var wrappedValue: Value {
        get {
            lock.lock()
            defer { lock.unlock() }
            return value
        }
        set {
            lock.lock()
            value = newValue
            lock.unlock()
        }
    }
}

Last but not least for primitive atomic types there is also Swift Atomics

Solution 3 - Ios

Kirsteins's answer is correct, but for convenience, I've updated that answer with Amol Chaudhari and Rob's suggestions for using a concurrent queue with async barrier to allow concurrent reads but block on writes.

I've also wrapped some other array functions that were useful to me.

public class SynchronizedArray<T> {
private var array: [T] = []
private let accessQueue = dispatch_queue_create("SynchronizedArrayAccess", DISPATCH_QUEUE_CONCURRENT)

public func append(newElement: T) {
    dispatch_barrier_async(self.accessQueue) {
        self.array.append(newElement)
    }
}

public func removeAtIndex(index: Int) {
    dispatch_barrier_async(self.accessQueue) {
        self.array.removeAtIndex(index)
    }
}

public var count: Int {
    var count = 0
    
    dispatch_sync(self.accessQueue) {
        count = self.array.count
    }
    
    return count
}

public func first() -> T? {
    var element: T?
    
    dispatch_sync(self.accessQueue) {
        if !self.array.isEmpty {
            element = self.array[0]
        }
    }
    
    return element
}

public subscript(index: Int) -> T {
    set {
        dispatch_barrier_async(self.accessQueue) {
            self.array[index] = newValue
        }
    }
    get {
        var element: T!
        
        dispatch_sync(self.accessQueue) {
            element = self.array[index]
        }
        
        return element
    }
}
}

UPDATE This is the same code, updated for Swift3.

public class SynchronizedArray<T> {
private var array: [T] = []
private let accessQueue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)

public func append(newElement: T) {
    
    self.accessQueue.async(flags:.barrier) {
        self.array.append(newElement)
    }
}

public func removeAtIndex(index: Int) {
    
    self.accessQueue.async(flags:.barrier) {
        self.array.remove(at: index)
    }
}

public var count: Int {
    var count = 0
    
    self.accessQueue.sync {
        count = self.array.count
    }
    
    return count
}

public func first() -> T? {
    var element: T?
    
    self.accessQueue.sync {
        if !self.array.isEmpty {
            element = self.array[0]
        }
    }
    
    return element
}

public subscript(index: Int) -> T {
    set {
        self.accessQueue.async(flags:.barrier) {
            self.array[index] = newValue
        }
    }
    get {
        var element: T!
        self.accessQueue.sync {
            element = self.array[index]
        }
        
        return element
    }
}
}

Solution 4 - Ios

My approach to this problem was using serial dispatch queue, to synchronise access to boxed array. It will block the thread when you try to get the value at index and queue is really busy, but that's the problem with locks as well.

public class SynchronizedArray<T> {
    private var array: [T] = []
    private let accessQueue = dispatch_queue_create("SynchronizedArrayAccess", DISPATCH_QUEUE_SERIAL)
    
    public func append(newElement: T) {
        dispatch_async(self.accessQueue) {
            self.array.append(newElement)
        }
    }
    
    public subscript(index: Int) -> T {
        set {
            dispatch_async(self.accessQueue) {
                self.array[index] = newValue
            }
        }
        get {
            var element: T!
            
            dispatch_sync(self.accessQueue) {
                element = self.array[index]
            }
            
            return element
        }
    }
}

var a = SynchronizedArray<Int>()
a.append(1)
a.append(2)
a.append(3)

// can be empty as this is non-thread safe access
println(a.array)

// thread-safe synchonized access
println(a[0])
println(a[1])
println(a[2])

Solution 5 - Ios

Details

  • Xcode 10.1 (10B61), Swift 4.2
  • Xcode 10.2.1 (10E1001), Swift 5

Solution

import Foundation

// https://developer.apple.com/documentation/swift/rangereplaceablecollection
struct AtomicArray<T>: RangeReplaceableCollection {

    typealias Element = T
    typealias Index = Int
    typealias SubSequence = AtomicArray<T>
    typealias Indices = Range<Int>
    fileprivate var array: Array<T>
    var startIndex: Int { return array.startIndex }
    var endIndex: Int { return array.endIndex }
    var indices: Range<Int> { return array.indices }

    func index(after i: Int) -> Int { return array.index(after: i) }

    private var semaphore = DispatchSemaphore(value: 1)
    fileprivate func _wait() { semaphore.wait() }
    fileprivate func _signal() { semaphore.signal() }
}

// MARK: - Instance Methods

extension AtomicArray {

    init<S>(_ elements: S) where S : Sequence, AtomicArray.Element == S.Element {
        array = Array<S.Element>(elements)
    }

    init() { self.init([]) }

    init(repeating repeatedValue: AtomicArray.Element, count: Int) {
        let array = Array(repeating: repeatedValue, count: count)
        self.init(array)
    }
}

// MARK: - Instance Methods

extension AtomicArray {

    public mutating func append(_ newElement: AtomicArray.Element) {
        _wait(); defer { _signal() }
        array.append(newElement)
    }

    public mutating func append<S>(contentsOf newElements: S) where S : Sequence, AtomicArray.Element == S.Element {
        _wait(); defer { _signal() }
        array.append(contentsOf: newElements)
    }

    func filter(_ isIncluded: (AtomicArray.Element) throws -> Bool) rethrows -> AtomicArray {
        _wait(); defer { _signal() }
        let subArray = try array.filter(isIncluded)
        return AtomicArray(subArray)
    }

    public mutating func insert(_ newElement: AtomicArray.Element, at i: AtomicArray.Index) {
        _wait(); defer { _signal() }
        array.insert(newElement, at: i)
    }

    mutating func insert<S>(contentsOf newElements: S, at i: AtomicArray.Index) where S : Collection, AtomicArray.Element == S.Element {
        _wait(); defer { _signal() }
        array.insert(contentsOf: newElements, at: i)
    }

    mutating func popLast() -> AtomicArray.Element? {
        _wait(); defer { _signal() }
        return array.popLast()
    }

    @discardableResult mutating func remove(at i: AtomicArray.Index) -> AtomicArray.Element {
        _wait(); defer { _signal() }
        return array.remove(at: i)
    }

    mutating func removeAll() {
        _wait(); defer { _signal() }
        array.removeAll()
    }

    mutating func removeAll(keepingCapacity keepCapacity: Bool) {
        _wait(); defer { _signal() }
        array.removeAll()
    }

    mutating func removeAll(where shouldBeRemoved: (AtomicArray.Element) throws -> Bool) rethrows {
        _wait(); defer { _signal() }
        try array.removeAll(where: shouldBeRemoved)
    }

    @discardableResult mutating func removeFirst() -> AtomicArray.Element {
        _wait(); defer { _signal() }
        return array.removeFirst()
    }

    mutating func removeFirst(_ k: Int) {
        _wait(); defer { _signal() }
        array.removeFirst(k)
    }

    @discardableResult mutating func removeLast() -> AtomicArray.Element {
        _wait(); defer { _signal() }
        return array.removeLast()
    }

    mutating func removeLast(_ k: Int) {
        _wait(); defer { _signal() }
        array.removeLast(k)
    }
    
    @inlinable public func forEach(_ body: (Element) throws -> Void) rethrows {
        _wait(); defer { _signal() }
        try array.forEach(body)
    }

    mutating func removeFirstIfExist(where shouldBeRemoved: (AtomicArray.Element) throws -> Bool) {
        _wait(); defer { _signal() }
        guard let index = try? array.firstIndex(where: shouldBeRemoved) else { return }
        array.remove(at: index)
    }

    mutating func removeSubrange(_ bounds: Range<Int>) {
        _wait(); defer { _signal() }
        array.removeSubrange(bounds)
    }

    mutating func replaceSubrange<C, R>(_ subrange: R, with newElements: C) where C : Collection, R : RangeExpression, T == C.Element, AtomicArray<Element>.Index == R.Bound {
        _wait(); defer { _signal() }
        array.replaceSubrange(subrange, with: newElements)
    }

    mutating func reserveCapacity(_ n: Int) {
        _wait(); defer { _signal() }
        array.reserveCapacity(n)
    }

    public var count: Int {
        _wait(); defer { _signal() }
        return array.count
    }

    public var isEmpty: Bool {
        _wait(); defer { _signal() }
        return array.isEmpty
    }
}

// MARK: - Get/Set

extension AtomicArray {

    // Single  action

    func get() -> [T] {
        _wait(); defer { _signal() }
        return array
    }

    mutating func set(array: [T]) {
        _wait(); defer { _signal() }
        self.array = array
    }

    // Multy actions

    mutating func get(closure: ([T])->()) {
        _wait(); defer { _signal() }
        closure(array)
    }

    mutating func set(closure: ([T]) -> ([T])) {
        _wait(); defer { _signal() }
        array = closure(array)
    }
}

// MARK: - Subscripts

extension AtomicArray {

    subscript(bounds: Range<AtomicArray.Index>) -> AtomicArray.SubSequence {
        get {
            _wait(); defer { _signal() }
            return AtomicArray(array[bounds])
        }
    }

    subscript(bounds: AtomicArray.Index) -> AtomicArray.Element {
        get {
            _wait(); defer { _signal() }
            return array[bounds]
        }
        set(value) {
            _wait(); defer { _signal() }
            array[bounds] = value
        }
    }
}

// MARK: - Operator Functions

extension AtomicArray {

    static func + <Other>(lhs: Other, rhs: AtomicArray) -> AtomicArray where Other : Sequence, AtomicArray.Element == Other.Element {
        return AtomicArray(lhs + rhs.get())
    }

    static func + <Other>(lhs: AtomicArray, rhs: Other) -> AtomicArray where Other : Sequence, AtomicArray.Element == Other.Element {
        return AtomicArray(lhs.get() + rhs)
    }

    static func + <Other>(lhs: AtomicArray, rhs: Other) -> AtomicArray where Other : RangeReplaceableCollection, AtomicArray.Element == Other.Element {
        return AtomicArray(lhs.get() + rhs)
    }

    static func + (lhs: AtomicArray<Element>, rhs: AtomicArray<Element>) -> AtomicArray {
        return AtomicArray(lhs.get() + rhs.get())
    }

    static func += <Other>(lhs: inout AtomicArray, rhs: Other) where Other : Sequence, AtomicArray.Element == Other.Element {
        lhs._wait(); defer { lhs._signal() }
        lhs.array += rhs
    }
}

// MARK: - CustomStringConvertible

extension AtomicArray: CustomStringConvertible {
    var description: String {
        _wait(); defer { _signal() }
        return "\(array)"
    }
}

// MARK: - Equatable

extension AtomicArray where Element : Equatable {

    func split(separator: Element, maxSplits: Int, omittingEmptySubsequences: Bool) -> [ArraySlice<Element>] {
        _wait(); defer { _signal() }
        return array.split(separator: separator, maxSplits: maxSplits, omittingEmptySubsequences: omittingEmptySubsequences)
    }

    func firstIndex(of element: Element) -> Int? {
        _wait(); defer { _signal() }
        return array.firstIndex(of: element)
    }

    func lastIndex(of element: Element) -> Int? {
        _wait(); defer { _signal() }
        return array.lastIndex(of: element)
    }

    func starts<PossiblePrefix>(with possiblePrefix: PossiblePrefix) -> Bool where PossiblePrefix : Sequence, Element == PossiblePrefix.Element {
        _wait(); defer { _signal() }
        return array.starts(with: possiblePrefix)
    }

    func elementsEqual<OtherSequence>(_ other: OtherSequence) -> Bool where OtherSequence : Sequence, Element == OtherSequence.Element {
        _wait(); defer { _signal() }
        return array.elementsEqual(other)
    }

    func contains(_ element: Element) -> Bool {
        _wait(); defer { _signal() }
        return array.contains(element)
    }

    static func != (lhs: AtomicArray<Element>, rhs: AtomicArray<Element>) -> Bool {
        lhs._wait(); defer { lhs._signal() }
        rhs._wait(); defer { rhs._signal() }
        return lhs.array != rhs.array
    }

    static func == (lhs: AtomicArray<Element>, rhs: AtomicArray<Element>) -> Bool {
        lhs._wait(); defer { lhs._signal() }
        rhs._wait(); defer { rhs._signal() }
        return lhs.array == rhs.array
    }
}

Usage sample 1

import Foundation

// init
var array = AtomicArray<Int>()
print(array)
array = AtomicArray(repeating: 0, count: 5)
print(array)
array = AtomicArray([1,2,3,4,5,6,7,8,9])
print(array)

// add
array.append(0)
print(array)
array.append(contentsOf: [5,5,5])
print(array)

// filter
array = array.filter { $0 < 7 }
print(array)

// map
let strings = array.map { "\($0)" }
print(strings)

// insert
array.insert(99, at: 5)
print(array)
array.insert(contentsOf: [2, 2, 2], at: 0)
print(array)

// pop
_ = array.popLast()
print(array)
_ = array.popFirst()
print(array)

// remove
array.removeFirst()
print(array)
array.removeFirst(3)
print(array)
array.remove(at: 2)
print(array)
array.removeLast()
print(array)
array.removeLast(5)
print(array)
array.removeAll { $0%2 == 0 }
print(array)
array = AtomicArray([1,2,3,4,5,6,7,8,9,0])
array.removeSubrange(0...2)
print(array)
array.replaceSubrange(0...2, with: [0,0,0])
print(array)
array.removeAll()
print(array)

array.set(array: [1,2,3,4,5,6,7,8,9,0])
print(array)

// subscript
print(array[0])
array[0] = 100
print(array)
print(array[1...4])

// operator functions
array = [1,2,3] + AtomicArray([4,5,6])
print(array)
array = AtomicArray([4,5,6]) + [1,2,3]
print(array)
array = AtomicArray([1,2,3]) + AtomicArray([4,5,6])
print(array)

Usage sample 2

import Foundation

var arr = AtomicArray([0,1,2,3,4,5])
for i in 0...1000 {
    // Single actions
    DispatchQueue.global(qos: .background).async {
        usleep(useconds_t(Int.random(in: 100...10000)))
        let num = i*i
        arr.append(num)
        print("arr.append(\(num)), background queue")
    }
    DispatchQueue.global(qos: .default).async {
        usleep(useconds_t(Int.random(in: 100...10000)))
        arr.append(arr.count)
        print("arr.append(\(arr.count)), default queue")
    }

    // multy actions
    DispatchQueue.global(qos: .utility).async {
        arr.set { array -> [Int] in
            var newArray = array
            newArray.sort()
            print("sort(), .utility queue")
            return newArray
        }
    }
}

Solution 6 - Ios

A minor detail: In Swift 3 (at least in Xcode 8 Beta 6), the syntax for queues changed significantly. The important changes to @Kirsteins' answer will be:

private let accessQueue = DispatchQueue(label: "SynchronizedArrayAccess")

txAccessQueue.async() {
  // Your async code goes here...
}

txAccessQueue.sync() {
  // Your sync code goes here...
}

Solution 7 - Ios

Swift-Nio & Vapor Swift

For those of you using Swift-Nio (or Vapor Swift which is based on Swift-Nio), there's a built in solution for this problem:

class MyClass {
    let lock = Lock()
    var myArray: Array<Int> = []

    func networkRequestWhatEver() {
        lock.withLock {
            array.append(someValue)
        }
    }
}

Note that you should use the same Lock object when modifing the same Array object (or Dictionary, etc.).

https://github.com/apple/swift-nio/blob/5e728b57862ce9e13877ff1edc9249adc933070a/Sources/NIOConcurrencyHelpers/lock.swift#L15

Solution 8 - Ios

Thread-safe Data Structures with Actors

As of Swift 5.5 you can express this with an actor:

actor SyncArray<T> {
    private var buffer: [T]
    
    init<S: Sequence>(_ elements: S) where S.Element == T {
        buffer = Array(elements)
    }
    
    var count: Int {
        buffer.count
    }
    
    func append(_ element: T) {
        buffer.append(element)
    }
    
    @discardableResult
    func remove(at index: Int) -> T {
        buffer.remove(at: index)
    }
}

Not only it makes the code simpler and less error prone, but it makes more explicit the potential race condition pointed out in an other answer:

Task {
    let array = SyncArray([1])

    if await array.count == 1 { 
        await array.remove(at: 0)
    }
}

There are two suspension points here, meaning that by the time .remove(at:) is called, the array count could have changed.

Such read-then-write operation must be atomic to be consistent, thus it should be defined as a method on the actor instead:

extension SyncArray {
    func removeLastIfSizeOfOne() {
        if buffer.count == 1 {
            buffer.remove(at: 0)
        }
    }
}

Above, the absence of suspension points indicates that the operation is performed atomically. Another solution that works without writing an extension is to use the isolated keyword like this:

func removeLastIfSizeOfOne<T>(_ array: isolated SyncArray<T>) {
    if array == 1 {
        array(at: 0)
    }
}

This will isolate the passed actor for the duration of the whole call instead of at each of its suspension points. Calling this function requires only one suspension point.

Solution 9 - Ios

Here is the answer for Swift 4,

let queue = DispatchQueue(label: "com.readerWriter", qos: .background, attributes: .concurrent)
var safeArray: [String] = []

subscript(index: Int) -> String {

    get {
        queue.sync {
            return safeArray[index]
        }
    }

    set(newValue) {
        queue.async(flags: .barrier) { [weak self] in
            self?.safeArray[index] = newValue
        }
    }
}

Solution 10 - Ios

I think dispatch_barriers are worth looking into. Using gcd for synchronicity is more intuitive to me than using synchronize keyword to avoid state mutation from multiple threads.

https://mikeash.com/pyblog/friday-qa-2011-10-14-whats-new-in-gcd.html

Solution 11 - Ios

Approach:

Use DispatchQueue to synchronise

Refer:

http://basememara.com/creating-thread-safe-arrays-in-swift/

Code:

Below is a crude implementation of a thread safe array, you can fine tune it.

public class ThreadSafeArray<Element> {
    
    private var elements    : [Element]
    private let syncQueue   = DispatchQueue(label: "Sync Queue",
                                            qos: .default,
                                            attributes: .concurrent,
                                            autoreleaseFrequency: .inherit,
                                            target: nil)
    
    public init() {
        elements = []
    }
    
    public init(_ newElements: [Element]) {
        elements = newElements
    }
    
    //MARK: Non-mutating
    
    public var first : Element? {
        return syncQueue.sync {
            elements.first
        }
    }
    
    public var last : Element? {
        return syncQueue.sync {
            elements.last
        }
    }
    
    public var count : Int {
        
        return syncQueue.sync {
            elements.count
        }
    }
    
    public subscript(index: Int) -> Element {
        
        get {
            return syncQueue.sync {
                elements[index]
            }
        }
        
        set {
            syncQueue.sync(flags: .barrier) {
                elements[index] = newValue
            }
        }
    }
    
    public func reversed() -> [Element] {
        
        return syncQueue.sync {
        
            elements.reversed()
        }
    }
    
    public func flatMap<T>(_ transform: (Element) throws -> T?) rethrows -> [T]  {
        
        return try syncQueue.sync {
        
           try elements.flatMap(transform)
        }
    }
    
    public func filter(_ isIncluded: (Element) -> Bool) -> [Element] {
        
        return syncQueue.sync {
         
            elements.filter(isIncluded)
        }
    }
    
    //MARK: Mutating
    
    public func append(_ element: Element) {
    
        syncQueue.sync(flags: .barrier) {
            
            elements.append(element)
        }
    }
    
    public func append<S>(contentsOf newElements: S) where Element == S.Element, S : Sequence {
        
        syncQueue.sync(flags: .barrier) {
            
            elements.append(contentsOf: newElements)
        }
    }
    
    public func remove(at index: Int) -> Element? {

        var element : Element?

        syncQueue.sync(flags: .barrier) {
            
            if elements.startIndex ..< elements.endIndex ~= index {
                element = elements.remove(at: index)
            }
            else {
                element = nil
            }
        }
        
        return element
    }
}

extension ThreadSafeArray where Element : Equatable {
    
    public func index(of element: Element) -> Int? {
        
        return syncQueue.sync {
            elements.index(of: element)
        }
    }
}

Solution 12 - Ios

firstly, objc_sync_enter not works

objc_sync_enter(array)
defer {
   objc_sync_exit(array)
}

reason https://stackoverflow.com/questions/35084754/objc-sync-enter-objc-sync-exit-not-working-with-dispatch-queue-priority-low

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.

for swift, should use like this, just as @Kirsteins said, and I suggest sync instead of async:

private let syncQueue = DispatchQueue(label:"com.test.LockQueue") 
func test(){
    self.syncQueue.sync{
        // thread safe code here
    }
}

Solution 13 - Ios

To improve the accepted answer I would suggest using defer:

objc_sync_enter(array)
defer {
   objc_sync_exit(array)
}
// manipulate the array

and the second one

func sync(lock: NSObject, closure: () -> Void) {
    objc_sync_enter(lock)
    defer {
        objc_sync_exit(lock)
    }
    closure()
}

Solution 14 - Ios

Swift Thread safe collection

The main and most common idea of making something(e.g. Collection) thread safe in Swift is:

  1. Custom(local) concurrent queue
  2. Synchronous reading. Reading a critical section(shared resource) via sync
  3. Asynchronous writing with barrier

[Swift Thread safe singleton]

Solution 15 - Ios

If you want thread-safe interaction with your array, you must synchronize your access. There are many alternatives suggested (and a few that have been omitted), so let us survey the various synchronization alternatives:

  1. Serial dispatch queue: This is a straightforward and intuitive GCD pattern.

  2. Reader-writer pattern with concurrent queue: This is an elegant refinement of the serial dispatch queue pattern, using concurrent queue with asynchronous “writes” (so the caller does not wait for the write to finish) with a barrier (to prevent any interaction concurrent with a “write”), but it offers concurrent “reads” (allowing greater concurrency during “reads”). This is a sophisticated and appealing pattern, but in practice, it is only useful if the benefits of concurrent “reads” and asynchronous “writes” outweigh the GCD overhead.

  3. Locks:

    • NSLock is a fast and simple locking mechanism that is more performant than any of the GCD alternatives for most scenarios:

      extension NSLocking {
          func synchronized<T>(_ block: () throws -> T) rethrows -> T {
              lock()
              defer { unlock() }
              return try block()
          }
      }
      
    • os_unfair_lock is another locking mechanism, which is even faster than NSLock, but is a little more complicated to use from Swift. See https://stackoverflow.com/a/66525671/1271826. But in those rare cases where performance is of paramount concern, unfair locks are a compelling solution.

  4. The Objective-C objc_sync_enter and objc_sync_exit API: This is not of practical interest in the Swift world.

  5. Semaphores: It is conceptually similar to lock-based approaches, but is generally slower than any of the lock-based approaches and can be disregarded in this conversation.

  6. Actors: A synchronization mechanism provided by the Swift 5.5 concurrency system. See Protect mutable state with Swift actors.

In short, if using async-await, actors are the logical alternative. If not yet adopting the Swift concurrency system, I would gravitate to a lock-based approach (which is simple and fast) or, in rare cases, the GCD reader-writer approach.

In practice, the choice of synchronization mechanism is not relevant in most use cases. (And if you are doing so many synchronizations that the performance difference becomes material, you might want to consider how to reduce the number of synchronization points before dwelling on the particular mechanism.) That having been said, the older synchronization mechanisms (semaphores, objc_sync_enter, etc.) simply would not be contemplated anymore.


Having outlined the possible synchronization mechanisms, the next question is at what level one performs the synchronization. Specifically, more than once, property wrappers for the entire array have been proposed. This is, invariably, the wrong place to synchronize. The property wrapper approach provides atomic access to the array (which is not quite the same thing as thread-safety), but you generally need a higher level of abstraction. E.g. if one thread is adding elements and while another is reading or removing, you often want each of these high-level tasks to be synchronized, not just the individual accesses of the array.

Solution 16 - Ios

There's a great answer here which is threadsafe and doesn't block concurrent reads: https://stackoverflow.com/a/15936959/2050665

It's written in Objective C, but porting to Swift is trivial.

@property (nonatomic, readwrite, strong) dispatch_queue_t thingQueue;
@property (nonatomic, strong) NSObject *thing;

- (id)init {
  ...
    _thingQueue = dispatch_queue_create("...", DISPATCH_QUEUE_CONCURRENT);
  ...
}

- (NSObject *)thing {
  __block NSObject *thing;
  dispatch_sync(self.thingQueue, ^{
    thing = _thing;
  });
  return thing;
}

- (void)setThing:(NSObject *)thing {
  dispatch_barrier_async(self.thingQueue, ^{
    _thing = thing;
  });
}

Credit to https://stackoverflow.com/users/97337/rob-napier

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionpatrickSView Question on Stackoverflow
Solution 1 - IosskimView Answer on Stackoverflow
Solution 2 - IosEraView Answer on Stackoverflow
Solution 3 - IosrmooneyView Answer on Stackoverflow
Solution 4 - IosKirsteinsView Answer on Stackoverflow
Solution 5 - IosVasily BodnarchukView Answer on Stackoverflow
Solution 6 - IosnbloqsView Answer on Stackoverflow
Solution 7 - Iosswift-lynxView Answer on Stackoverflow
Solution 8 - IosLouis LacView Answer on Stackoverflow
Solution 9 - IosSathish Kumar GurunathanView Answer on Stackoverflow
Solution 10 - Iosamol-cView Answer on Stackoverflow
Solution 11 - Iosuser1046037View Answer on Stackoverflow
Solution 12 - IoslbsweekView Answer on Stackoverflow
Solution 13 - IosVyacheslavView Answer on Stackoverflow
Solution 14 - IosyoAlex5View Answer on Stackoverflow
Solution 15 - IosRobView Answer on Stackoverflow
Solution 16 - IosRiver SatyaView Answer on Stackoverflow