21

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)
    }
}
mfaani
  • 33,269
  • 19
  • 164
  • 293
Andrew
  • 7,693
  • 11
  • 43
  • 81

5 Answers5

41

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • **1)** This is all happening because we are wrapping an Array into a class and therefore it loses its thread safety and is no longer copied right? **2** your 1st solution, why do the reads have to be from the same queue? Isn't it *writing* that creates the issue? **3)** `dispatch_barrier_async(blocksDispatchQueue)` even though this has `async` I'm guessing it's still somewhat a serial queue because you said : * writes are synchronized*. I read the docs on `dispatch_barrier_async` but still confused of what the code does **&** how you can achieve synchronization using a concurrent queue? – mfaani Dec 27 '16 at 17:11
  • 1
    @Honey - 1. The need to synchronize to achieve thread-safety concern has nothing to do with the fact that the array is in class or not. It's just that an array, which is not, itself, thread-safe, is being accessed from multiple threads, so all interactions must be synchronized; 2. In order to achieve thread safety, writes must be synchronized with respect to _all_ interaction with that object, so both reads and writes must be performed in the queue in question; 3. for reader-writer, see https://developer.apple.com/videos/play/wwdc2011/210/ or https://developer.apple.com/videos/wwdc/2012/712. – Rob Dec 27 '16 at 17:34
  • 1. from [this](http://stackoverflow.com/a/38903479/5175709) answer : *every thread gets its own copy.That means that every thread can read and write to its instance without having to worry about what other threads are doing.* You seem to be saying the exact opposite, but likely it's something I don't understand :D 2. OK 3. the videos explains dispatch_barrier_async? OK, will see and get back to you – mfaani Dec 27 '16 at 17:41
  • a good short explanation on `dispatch_barrier_async` can be found [here](https://www.quora.com/Whats-the-difference-between-dispatch_barrier_async-and-dispatch_async/answer/Manuel-Costa-2?srid=PdV3) – mfaani Dec 27 '16 at 18:10
  • 1
    @Honey - "every thread gets its own copy" ... That's a bit oversimplified. But Josh is right, that if you use value types and appropriate value semantics, then that eliminates the need to synchronize. But if one thread is updating an array (presumably so you can see that edit from another queue), that simply doesn't apply. You still need to synchronize. Frankly, I'd suggest that if you have any further questions, post your own question rather continuing to post comments here. – Rob Dec 27 '16 at 18:51
  • Apologies, for future readers, I wrote [this](https://stackoverflow.com/questions/41350772/if-arrays-are-value-types-and-therefore-get-copied-then-how-are-they-not-thread) question... – mfaani Dec 27 '16 at 19:06
  • I had a hard time understanding the answer to my question2. so this how I'd answer it. if you are purely doing read, ie from a 1000 threads you are reading then you don't have a problem. However if you are doing read-write at the same time...you have problems. – mfaani Dec 28 '16 at 21:06
  • Yep. That's the heart of the reader-writer pattern: All access is synchronized through a concurrent queue where reads can happen concurrently with respect to each other (i.e. maximum efficiency), but a write must not happen concurrently with respect to any other access to that object (whether reads or other writes). – Rob Dec 28 '16 at 21:18
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/131751/discussion-between-honey-and-rob). – mfaani Dec 28 '16 at 22:36
  • @Rob Hey Rob, do you still recommend this way to append items to an array by multiple threads or you prefer Semaphores nowadays? – J. Doe Jan 11 '19 at 20:43
  • @J.Doe - I wouldn’t advise semaphores as they’re easily used incorrectly and are no more efficient. But what I might consider nowadays, is `NSLock`. See revised answer. When I benchmarked this many years ago, it was much slower than GCD, but nowadays the tables have turned. – Rob Aug 21 '19 at 19:03
3

For synchronization between threads, use dispatch_sync (not _async) and your own dispatch queue (not the global one):

class MyArrayBlockClass {
    private var queue = dispatch_queue_create("andrew.myblockarrayclass", nil)

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

dispatch_sync is nice and easy to use and should be enough for your case (I use it for all my thread synchronization needs at the moment), but you can also use lower-level locks and mutexes. There is a great article by Mike Ash about different choices: Locks, Thread Safety, and Swift

Teemu Kurppa
  • 4,779
  • 2
  • 32
  • 38
  • 3
    When updating the object, `dispatch_async` (or `dispatch_barrier_async`) is fine. It's only for reads that you must use `dispatch_sync`. – Rob Aug 31 '16 at 20:29
1

Create a serial queue and make changes to the array in that thread. Your thread creation call should be something like this

private let blocksDispatchQueue = dispatch_queue_create("SynchronizedArrayAccess", DISPATCH_QUEUE_SERIAL)

Then you can use it the same way how are right now.

func addBlockToArray(block: () -> Void) {
    dispatch_async(blocksDispatchQueue) {
        self.blocksArray.append(block)
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
Sumeet
  • 1,055
  • 8
  • 18
1

Details

  • Xcode 10.1 (10B61)
  • Swift 4.2

Solution

import Foundation

class AtomicArray<T> {

    private lazy var semaphore = DispatchSemaphore(value: 1)
    private var array: [T]

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

    func append(newElement: T) {
        wait(); defer { signal() }
        array.append(newElement)
    }

    subscript(index: Int) -> T {
        get {
            wait(); defer { signal() }
            return array[index]
        }
        set(newValue) {
            wait(); defer { signal() }
            array[index] = newValue
        }
    }

    var count: Int {
        wait(); defer { signal() }
        return array.count
    }

    private func wait() { semaphore.wait() }
    private func signal() { semaphore.signal() }

    func set(closure: (_ curentArray: [T])->([T]) ) {
        wait(); defer { signal() }
        array = closure(array)
    }

    func get(closure: (_ curentArray: [T])->()) {
        wait(); defer { signal() }
        closure(array)
    }

    func get() -> [T] {
        wait(); defer { signal() }
        return array
    }
}

extension AtomicArray: CustomStringConvertible {
    var description: String { return "\(get())"}
}

Usage

The basic idea is to use the syntax of a regular array

let atomicArray = AtomicArray(array: [3,2,1])

 print(atomicArray)
 atomicArray.append(newElement: 1)

 let arr = atomicArray.get()
 print(arr)
 atomicArray[2] = 0

 atomicArray.get { currentArray in
      print(currentArray)
 }

 atomicArray.set { currentArray -> [Int] in
      return currentArray.map{ item -> Int in
           return item*item
      }
 }
 print(atomicArray)

Usage result

enter image description here

Full sample

import UIKit

class ViewController: UIViewController {

    var atomicArray = AtomicArray(array: [Int](repeating: 0, count: 100))

    let dispatchGroup = DispatchGroup()

    override func viewDidLoad() {
        super.viewDidLoad()

        arrayInfo()

        sample { index, dispatch in
            self.atomicArray[index] += 1
        }

        dispatchGroup.notify(queue: .main) {
            self.arrayInfo()
            self.atomicArray.set { currentArray -> ([Int]) in
                return currentArray.map{ (item) -> Int in
                    return item + 100
                }
            }
           self.arrayInfo()
        }

    }

    private func arrayInfo() {
        print("Count: \(self.atomicArray.count)\nData: \(self.atomicArray)")
    }

    func sample(closure: @escaping (Int,DispatchQueue)->()) {

        print("----------------------------------------------\n")

        async(dispatch: .main, closure: closure)
        async(dispatch: .global(qos: .userInitiated), closure: closure)
        async(dispatch: .global(qos: .utility), closure: closure)
        async(dispatch: .global(qos: .default), closure: closure)
        async(dispatch: .global(qos: .userInteractive), closure: closure)
    }

    private func async(dispatch: DispatchQueue, closure: @escaping (Int,DispatchQueue)->()) {

        for index in 0..<atomicArray.count {
            dispatchGroup.enter()
            dispatch.async {
                closure(index,dispatch)
                self.dispatchGroup.leave()
            }
        }
    }
}

Full sample result

enter image description here

Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
  • 1
    too broad answer... while the meaningful part is only "guard with dispatch semaphore", e.g. https://stackoverflow.com/a/44951405/2078908. – ursa May 13 '20 at 21:53
0

NSOperationQueue itself is thread safe, so you could set suspended to true, add all the blocks you want from any thread, and then set suspended to false to run all the blocks.

Jack Lawrence
  • 10,664
  • 1
  • 47
  • 61