Create Thread Safe Array in Swift

Creating a threadsafe Array, the easy way?

The synchronization mechanism in your question, with concurrent queue and judicious use of barrier is known as the “reader-writer” pattern. In short, it offers concurrent synchronous reads and non-concurrent asynchronous writes. This is a fine synchronization mechanism. It is not the problem here.

But there are a few problems:

  1. In the attempt to pare back the implementation, this class has become very inefficient. Consider:

    class ThreadSafeArray {
    private var array: [Element]
    private let queue = DispatchQueue(label: "ThreadsafeArray.reader-writer", attributes: .concurrent)

    init(_ array: [Element] = []) {
    self.array = array
    }
    }

    extension ThreadSafeArray {
    var threadsafe: [Element] {
    get { queue.sync { array } }
    set { queue.async(flags: .barrier) { self.array = newValue } }
    }
    }

    let numbers = ThreadSafeArray([1, 2, 3])
    numbers.threadsafe[1] = 42 // !!!

    What that numbers.threadsafe[1] = 42 line is really doing is as follows:

    • Fetching the whole array;
    • Changing the second item in a copy of the array; and
    • Replacing the whole array with a copy of the array that was just created.

    That is obviously very inefficient.

  2. The intuitive solution is to add an efficient subscript operator in the implementation:

    extension ThreadSafeArray {
    typealias Index = Int

    subscript(index: Index) -> Element {
    get { queue.sync { array[index] } }
    set { queue.async(flags: .barrier) { self.array[index] = newValue} }
    }
    }

    Then you can do:

    numbers[1] = 42

    That will perform a synchronized update of the existing array “in place”, without needing to copy the array at all. In short, it is an efficient, thread-safe mechanism.

    What will end up happening, as one adds more and more basic “array” functionality (e.g., especially mutable methods such as the removing of items, adding items, etc.), you end up with an implementation not dissimilar to the original implementation you found online. This is why that article you referenced implemented all of those methods: It exposes array-like functionality, but offering an efficient and (seemingly) thread-safe interface.

  3. While the above addresses the data races, there is a deep problem in that code sample you found online, as illuminated by your thread-safety test.

    To illustrate this, let’s first assume we flesh out our ThreadSafeArray to have last, append() and make it print-able:

    class ThreadSafeArray {
    private var array: [Element]
    private let queue = DispatchQueue(label: "ThreadsafeArray.reader-writer", attributes: .concurrent)

    init(_ array: [Element] = []) {
    self.array = array
    }
    }

    extension ThreadSafeArray {
    typealias Index = Int

    subscript(index: Index) -> Element {
    get { queue.sync { array[index] } }
    set { queue.async(flags: .barrier) { self.array[index] = newValue} }
    }

    var last: Element? {
    queue.sync { array.last }
    }

    func append(_ newElement: Element) {
    queue.async(flags: .barrier) {
    self.array.append(newElement)
    }
    }
    }

    extension ThreadSafeArray: CustomStringConvertible {
    var description: String {
    queue.sync { array.description }
    }
    }

    That implementation (a simplified version of the rendition found on that web site) looks OK, as it solves the data race and avoids unnecessary copying of the array. But it has its own problems. Consider this rendition of your thread-safety test:

    let numbers = ThreadSafeArray([0])

    DispatchQueue.concurrentPerform(iterations: 1_000) { <#Int#> in
    let lastValue = numbers.last! + 1
    numbers.append(lastValue)
    }

    print(numbers) // !!!

    The strict data race is solved, but the result will not be [0, 1, 2, ..., 1000]. The problem are the lines:

    let lastValue = numbers.last! + 1
    numbers.append(lastValue)

    That does a synchronized retrieval of last followed by a separate synchronized append. The problem is that another thread might slip in between these two synchronized calls and fetch the same last value! You need to wrap the whole “fetch last value, increment it, and append this new value” in a single, synchronized task.

    To solve this, we would often give the thread-safe object a method that would provide a way to perform multiple statements as a single, synchronized, task. E.g.:

    extension ThreadSafeArray {
    func synchronized(block: @escaping (inout [Element]) -> Void) {
    queue.async(flags: .barrier) { [self] in
    block(&array)
    }
    }
    }

    Then you can do:

    let numbers = ThreadSafeArray([0])

    DispatchQueue.concurrentPerform(iterations: 1_000) { <#Int#> in
    numbers.synchronized { array in
    let lastValue = array.last! + 1
    array.append(lastValue)
    }
    }

    print(numbers) // OK
  4. So let’s return to your intuition that the author’s class can be simplified. You are right, that it can and should be simplified. But my rationale is slightly different than yours.

    The complexity of the implementation is not my concern. It actually is an interesting pedagogical exercise to understand barriers and the broader reader-writer pattern.

    My concern is that (to my point 3, above), is that the author’s implementation lulls an application developer in a false sense of security provided by the low-level thread-safety. As your tests demonstrate, a higher-level level of synchronization is almost always needed.

    In short, I would stick to a very basic implementation, one that exposes the appropriate high-level, thread-safe interface, not a method-by-method and property-by-property interface to the underlying array, which almost always will be insufficient. In fact, this desire for a high-level, thread-safe interface is a motivating idea behind a more modern thread-safety mechanism, namely actors in Swift concurrency.

How to achieve thread safety for removeAtIndex when array has concurrent reads in Swift?

You said:

I am struggling to work out what @Rob means by “Your example is easily solved (by adding [a] method that dispatches block to the queue)”. I would be interested to see an example implementation of this method (or any other) technique to solve the problem.

Let’s expand upon the example that I posted in response to your other question (see point 3 in this answer), adding a few more Array methods:

class SynchronizedArray {
private var array: [T]
private let accessQueue = DispatchQueue(label: "com.domain.app.reader-writer", attributes: .concurrent)

init(_ array: [T] = []) {
self.array = array
}

subscript(index: Int) -> T {
get { reader { $0[index] } }
set { writer { $0[index] = newValue } }
}

var count: Int {
reader { $0.count }
}

func append(newElement: T) {
writer { $0.append(newElement) }
}

func remove(at index: Int) {
writer { $0.remove(at: index) }
}

func reader(_ block: ([T]) throws -> U) rethrows -> U {
try accessQueue.sync { try block(array) }
}

func writer(_ block: @escaping (inout [T]) -> Void) {
accessQueue.async(flags: .barrier) { block(&self.array) }
}
}

So, let’s imagine that you wanted to delete an item if there was only one item in the array. Consider:

let numbers = SynchronizedArray([42])

...

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

That looks innocent enough, but it is not thread-safe. You could have a race condition if other threads are inserting or removing values. E.g., if some other thread appended a value between the time you tested the count and when you went to remove the value.

You can fix that by wrapping the whole operation (the if test and the consequent removal) in a single block that is synchronized. Thus you could:

numbers.writer { array in
if array.count == 1 {
array.remove(at: 0)
}
}

This writer method (in this reader-writer-based synchronization) is an example of what I meant by a “method that dispatches block to the queue”.


Now, clearly, you could also give your SynchronizedArray its own method that did this for you, e.g.:

func safelyRemove(at index: Int) {
writer { array in
if index < array.count {
array.remove(at: index)
}
}
}

Then you can do:

numbers.safelyRemove(at: index)

... and that is thread-safe, but still enjoys the performance benefits of reader-writer synchronization.

But the general idea is that when dealing with a thread-safe collection, you invariably have a series of tasks that you will want to synchronize together, at a higher level of abstraction. By exposing the synchronization methods of reader and writer, you have a simple, generalized mechanism for doing that.


All of that having been said, as others have said, the best way to write thread-safe code is to avoid concurrent access altogether. But if you must make a mutable object thread-safe, then it is the responsibility of the caller to identify the series of tasks that must be performed as a single, synchronized operation.

Swift thread safe array

Your problem is that unshift calls count. unshift is already holding the semaphore, but the first thing that count does is call wait, which causes a deadlock. You have the same problem in popLast.

Since you already have exclusive access to the array you can simply use its isEmpty property.

public func unshift() -> T? {
var firstEl:T? = nil
wait(); defer { signal() }
if !dataArray.isEmpty {
firstEl = dataArray.removeFirst()
}
return firstEl;
}

public func pop() -> T? {
var lastEl:T? = nil
wait(); defer { signal() }
if !dataArray.isEmpty {
lastEl = dataArray.popLast()
}
return lastEl;
}

You could also replace your DispatchSemaphore with a NSRecursiveLock since you don't need the counting behaviour of a semaphore. NSRecursiveLock can be locked multiple times by the same thread without causing a deadlock.

Is the Swift Array sorted method thread safe for a local copy of an array?

devices is not an Array. It's a Dictionary.Values. It's a lazy view into deviceDict.

But none of that matters. Mutable access to deviceDict on multiple threads is always undefined behavior. If it's possible for serialID to change on another thread without some kind of synchronization, you've already made an error. The deviceDict.values() line would be invalid; having nothing to do with later accesses to it.

But of course the sorted line is more likely to be where that undefined behavior bites you, since it's going to walk a lot of values, which might be moved in memory if deviceDict has to resize or otherwise modify its backing storage.

Array pass by value by default & thread-safety

1) In code example what you provided will be returned copy of _photos.
As wrote in article:

The getter for this property is termed a read method as it’s reading
the mutable array. The caller gets a copy of the array and is protected
against mutating the original array inappropriately.

that's mean what you can access to _photos from outside of class, but you can't change them from there. Values of photos could be changed only inside class what make this array protected from it accidental changing.

2)Yes, Array is a value-type struct and it will be passed by value. You can easily check it in Playground

let arrayA = [1, 2, 3]
var arrayB = arrayA

arrayB[1] = 4 //change second value of arrayB

print(arrayA) //but arrayA didn't change

UPD #1

In article they have method func addPhoto(_ photo: Photo) what add new photo to _photos array what makes access to this property not thread-safe. That's mean what value of _photos could be changed on few thread in same time what will lead to issues.

They fixed it by writing photos on concurrentQueue with .barrier what make it thread-safely, _photos array will changed once per time

func addPhoto(_ photo: Photo) {
concurrentPhotoQueue.async(flags: .barrier) { // 1
self._photos.append(photo) // 2
DispatchQueue.main.async { // 3
self.postContentAddedNotification()
}
}
}

Now for ensure thread safety you need to read of _photos array on same queue. That's only reason why they refactored read method

Swift mutable array thread safety

The exact details about thread-safety should be specified by the language. Currently, there is no such specification for Swift. There's even no such thing like a "thread". Thus, we should apply the "worst case" (e.g. how C would behave) and apply those knowledge from GCD, and other C APIs that can be used in Swift.

Notice, that some language idioms may be thread-safe only because the language and the underlying toolset itself takes care of it, e.g. inserting calls to appropriate memory-barriers instructions when required for the language construct and the current hardware when it generates the code for it. The language C (and C++) does nothing in this respect on our behalf.

What we strive to avoid is a "data race". A data race can happen when any thread writes to a memory location and any other thread reads from the same location without using explicit synchronisation primitives. And, think of a "thread" as the same thing we mean when creating a NSThread object in Cocoa.

So, your question whether your scenario is thread-safe is a clear "No, it is not." What you need is some form of concurrency control, e.g. using a dedicated serial dispatch queue where you execute the access (reading and writing) to the array, or use locks in conjunction with a mutex (aka a "critical section").



Related Topics



Leave a reply



Submit