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)
}
}
You have defined your blocksDispatchQueue
to be a global queue. Updating this for Swift 3, the equivalent is:
private let blocksDispatchQueue = DispatchQueue.global()
func addBlockToArray(block: @escaping () -> Void) {
blocksDispatchQueue.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 blocksDispatchQueue = 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() {
blocksDispatchQueue.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 blocksDispatchQueue = 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) {
blocksDispatchQueue.async(flags: .barrier) {
self.blocksArray.append(block)
}
}
But you can now read data, like above:
someVariable = blocksDispatchQueue.sync {
self.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.
If you're looking for Swift 2 examples, see the previous rendition of this answer.
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
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)
}
}
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.
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
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
来源:https://stackoverflow.com/questions/28784507/adding-items-to-swift-array-across-multiple-threads-causing-issues-because-arra