Adding items to Swift array across multiple threads causing issues (because arrays aren't thread safe) - how do I get around that?

Andrew picture Andrew · Feb 28, 2015 · Viewed 11.7k times · Source

I want to add given blocks to an array, and then run all the blocks contained in the array, when requested. I have code similar to this:

class MyArrayBlockClass {
    private var blocksArray: Array<() -> Void> = Array()

    private let blocksQueue: NSOperationQueue()

    func addBlockToArray(block: () -> Void) {
        self.blocksArray.append(block)
    }

    func runBlocksInArray() {
        for block in self.blocksArray {
            let operation = NSBlockOperation(block: block)
            self.blocksQueue.addOperation(operation)
        }

        self.blocksQueue.removeAll(keepCapacity: false)
    }
}

The problem comes with the fact that addBlockToArray can be called across multiple threads. What's happening is addBlockToArray is being called in quick succession across different threads, and is only appending one of the items, and so the other item is therefore not getting called during runBlocksInArray.

I've tried something like this, which doesn't seem to be working:

private let blocksDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

func addBlockToArray(block: () -> Void) {
    dispatch_async(blocksDispatchQueue) {
        self.blocksArray.append(block)
    }
}

Answer

Rob picture Rob · Feb 28, 2015

You have defined your blocksDispatchQueue to be a global queue. Updating this for Swift 3, the equivalent is:

private let queue = DispatchQueue.global()

func addBlockToArray(block: @escaping () -> Void) {
    queue.async {
        self.blocksArray.append(block)
    }
}

The problem is that global queues are concurrent queues, so you're not achieving the synchronization you want. But if you created your own serial queue, that would have been fine, e.g. in Swift 3:

private let queue = DispatchQueue(label: "com.domain.app.blocks")

This custom queue is, by default, a serial queue. Thus you will achieve the synchronization you wanted.

Note, if you use this blocksDispatchQueue to synchronize your interaction with this queue, all interaction with this blocksArray should be coordinated through this queue, e.g. also dispatch the code to add the operations using the same queue:

func runBlocksInArray() {
    queue.async {
        for block in self.blocksArray {
            let operation = BlockOperation(block: block)
            self.blocksQueue.addOperation(operation)
        }

        self.blocksArray.removeAll()
    }
}

Alternatively, you can also employ the reader/writer pattern, creating your own concurrent queue:

private let queue = DispatchQueue(label: "com.domain.app.blocks", attributes: .concurrent)

But in the reader-writer pattern, writes should be performed using barrier (achieving a serial-like behavior for writes):

func addBlockToArray(block: @escaping () -> Void) {
    queue.async(flags: .barrier) {
        self.blocksArray.append(block)
    }
}

But you can now read data, like above:

let foo = queue.sync {
    blocksArray[index]
}

The benefit of this pattern is that writes are synchronized, but reads can occur concurrently with respect to each other. That's probably not critical in this case (so a simple serial queue would probably be sufficient), but I include this read-writer pattern for the sake of completeness.


Another approach is NSLock:

extension NSLocking {
    func withCriticalSection<T>(_ closure: () throws -> T) rethrows -> T {
        lock()
        defer { unlock() }
        return try closure()
    }
}

And thus:

let lock = NSLock()

func addBlockToArray(block: @escaping () -> Void) {
    lock.withCriticalSection {
        blocksArray.append(block)
    }
}

But you can now read data, like above:

let foo = lock.withCriticalSection {
    blocksArray[index]
}

Historically NSLock was always dismissed as being less performant, but nowadays it is even faster than GCD.


If you're looking for Swift 2 examples, see the previous rendition of this answer.