Create thread safe array in Swift
Asked Answered
A

16

106

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.

Asch answered 28/1, 2015 at 11:26 Comment(1)
If you don't want a duplicate data in your Array, you use SetFarwell
I
125

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
Ironwood answered 11/3, 2015 at 0:42 Comment(8)
Thanks, but what are the advantages/disadvantages of objc_sync_enter vs dispatch_queue?Hultgren
objc_sync_enter is what's underneath Objective-C's @synchronized. Since I haven't really looked at what's underneath objc_sync_enter I am sort of trusting that the open source code works as promised. As for using serial queue, you know exactly how that is working (blocks of codes sequentially). I am guessing the downside of creating and managing a serial queue per thread safe collection type instance could be costly.Ironwood
You got me curious, so I found this on objc_sync_enter: rykap.com/objective-c/2015/05/09/synchronizedIronwood
this works perfect for tuples and NON AnyObject if you have Wrapper class like here https://mcmap.net/q/15836/-how-to-convert-tuple-to-anyobject-in-swiftLeveridge
It's not safe/correct to use objc_sync_enter on a value type such as Array. objc_sync_array does pointer comparison and the lifetime of the pointer of a bridged Swift value type is not guaranteed to be stable (and in fact most of the time it is not stable).Herefordshire
@Ironwood I wonder if we can do it like to? Will it save some hassel? ``` var name: String? { set { objc_sync_enter(innerName) innerName = newValue objc_sync_exit(innerName) } get { objc_sync_enter(innerName) let newValue = innerName objc_sync_exit(innerName) return newValue } } private(set) var innerName: String? ```Positivism
this not working, reason stackoverflow.com/questions/35084754/…Calcimine
Just wondering should we capture the self weakly in the blocks? Or is it fine and won't create retain cycle?Magda
I
68

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()
            defer { lock.unlock() }
            value = newValue
        }
    }
}

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

Irregularity answered 28/6, 2020 at 8:42 Comment(5)
It seems that adding a call to .lock() within the willSet property observer (and a corresponding .unlock() in didSet) works. This means you don't have to make any other code changes at call sites. (If you remove the call to .unlock() you can confirm that this does work). I imagine you could probably create a property wrapper for this too.Shower
Yes, you could make a @propertyWrapper tooIrregularity
@Shower if it will crash during a set, it will never unlock in didSet.Dominique
Heads up, the property wrapper won't work for any Collection, including Array. donnywals.com/…Orlosky
@AlexanderVolkov If it crashes during a set, then any thread locks are no longer relevant— the process and all threads are forceably ended by the kernel. Did you mean “if it will throw an exception during as set”?Allegedly
I
40

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
    }
}
}
Illampu answered 14/10, 2016 at 14:17 Comment(9)
removeAtIndex could remove wrong item in this code cause idx cold be changed by other thread 'removeAtIndex' ...Caudad
@Speakus could be wrong but I'm pretty sure it doesn't. It's async, but on the same concurrent queue as other array operations... so the removeAtIndex will happen before any other read or write operation happens.Flanders
@JordanSmith The issue will happen when you call removeAtIndex but code will switch to another thread for example in time when you get self.accessQueue but before .async running.Caudad
Can you please explain how to use this SynchronizedArray class in place of normal swift 3 array ([String] or []) ? It's very urgentTense
Use it like an array in a class that can be accessed from multiple threads. let array = SynchronizedArray<String>() array.append("New string") print(array[0]) array.removeAtIndex(index: 0) print(array.count)Illampu
This should be the correct answer. objc_sync_enter/exit won't give you concurrent reads and @rmooneys answer is pure SwiftBentonbentonite
@mooney I think @Speakus is right here: Say the sychronized array has one element, wouldn't this fail? if synchronizedArray.count == 1 { synchronizedArray.remove(at: 0) } It's a race condition, say two threads execute the statement. Both read a count of 1 concurrently, both enqueue a write block concurrently. The write blocks execute sequentially, the second one will fail. Am I wrong? Is this a designer or user error? In either case It's fragile as hell & making me never want to touch multi-threading... How would you even make this safe? Back to exclusive locks for both reading and writing?Danyelldanyelle
@Danyelldanyelle - You are absolutely right that this level of synchronization (at the property/method level) is frequently insufficient to achieve true thread-safety in broader applications. Your example is easily solved (by adding an method that dispatches block to the queue), but there are others that aren't (e.g. "synchronized" array simultaneously used by a UITableViewDataSource and mutated by some background operation). In those cases, you have to implement your own higher-level synchronization. But the above technique is nonetheless very useful in certain, highly constrained situations.Untune
@Illampu - In your Swift 3 and 4 examples, you can simplify some of those routines, eliminating the implicitly unwrapped optionals, e.g. var first: T? { return accessQueue.sync { self.array.first } }Untune
C
36

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])
Classified answered 28/1, 2015 at 11:48 Comment(3)
You might consider using reader-writer pattern: Use DISPATCH_QUEUE_CONCURRENT; changing writes from dispatch_async to dispatch_barrier_async; but leaving reads as dispatch_sync. This gives you concurrent reading, but writes are still synchronized.Untune
How would you pass in a sort for custom types?Tutty
Doesn't a DispatchQueue require the CPU to do a context switch to another thread, thus resulting in a very expensive operation? Or is it sync() that actually solves this problem by not dispatching to the queue if not necessary because it's a read-only resource access?Shah
M
10

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.count == 1 {
        array.remove(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.

Manassas answered 8/11, 2021 at 9:4 Comment(5)
Would it work if you add a subscript method?Kanchenjunga
It works with a read-only subscript but I wasn’t able to use a subscript setter.Manassas
what error did you get ?Kanchenjunga
"Actor-isolated subscript can not be mutated from the main actor/Sendable closure" depending on where I run the code.Manassas
I may be wrong but it looks like a Swift bug to me. The subscript should be run on the actor like any other instance method of an actor.Manassas
D
9

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
        }
    }
}
Dorchester answered 7/2, 2019 at 2:14 Comment(1)
This doesn't compile for meKanchenjunga
W
7

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...
}
Waldos answered 30/8, 2016 at 20:19 Comment(0)
C
6

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

Conde answered 26/3, 2020 at 14:4 Comment(3)
import NIOConcurrencyHelpersRanger
Why would you import a whole library just for locking?Irregularity
If one is using vapor swift and swift-nio already anyways, this is probably the easiest solution.Conde
A
4

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
        }
    }
}
Anana answered 14/1, 2020 at 2:32 Comment(1)
Don't use queues for locking. It just uses a lock internally and dispatches to a different thread which is completely unnecessary. Use a lock.Irregularity
D
2

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

Doggish answered 1/10, 2015 at 14:4 Comment(0)
I
1

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)
        }
    }
}
Infusible answered 23/5, 2017 at 9:7 Comment(4)
Which version of Swift are you using ?Infusible
My bad, I wrote comment without trying to compile the code. I though that return queue.sync { _array } won't compile because sync returns nothing.Broker
It returns array and the access to the array is synchronised.Infusible
Access to the array is synchronized, true, but any operations performed on the returned array are not.Gabar
C
1

firstly, objc_sync_enter not works

objc_sync_enter(array)
defer {
   objc_sync_exit(array)
}

reason 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
    }
}
Calcimine answered 3/5, 2018 at 7:39 Comment(2)
serial queue with sync wont cause deadlock ?Dr
thanks @karthik, sync did lead to deadlock in some scenario. but for my project, the queue using it has no resource dependency, so it is fine.Calcimine
U
1

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://mcmap.net/q/15838/-39-osspinlock-39-was-deprecated-in-ios-10-0-use-os_unfair_lock-from-lt-os-lock-h-gt-instead. 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.

Untune answered 24/1, 2022 at 17:40 Comment(0)
R
0

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()
}
Regulator answered 26/1, 2018 at 22:19 Comment(1)
this not works, see stackoverflow.com/questions/35084754/…Calcimine
C
0

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]

Camara answered 1/12, 2021 at 21:12 Comment(0)
D
-1

There's a great answer here which is threadsafe and doesn't block concurrent reads: https://mcmap.net/q/15839/-avoid-this-dangling-pointer-with-arc

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

Dimissory answered 16/9, 2016 at 3:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.