Agregar elementos a la matriz Swift en varios subprocesos que causan problemas (porque las matrices no son seguras para subprocesos): ¿cómo puedo evitar eso?

Quiero agregar bloques dados a una matriz, y luego ejecutar todos los bloques contenidos en la matriz, cuando así lo solicite. Tengo un código similar a esto:

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) } } 

El problema viene con el hecho de que se puede llamar a addBlockToArray en varios subprocesos. Lo que sucede es que se llama a addBlockToArray en sucesión rápida a través de diferentes subprocesos, y solo se agrega uno de los elementos, por lo que el otro elemento no se llama durante runBlocksInArray.

He intentado algo como esto, que no parece estar funcionando:

 private let blocksDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) func addBlockToArray(block: () -> Void) { dispatch_async(blocksDispatchQueue) { self.blocksArray.append(block) } } 

Ha definido sus blocksDispatchQueue para ser una cola global. Al actualizar esto para Swift 3, el equivalente es:

 private let blocksDispatchQueue = DispatchQueue.global() func addBlockToArray(block: @escaping () -> Void) { blocksDispatchQueue.async { self.blocksArray.append(block) } } 

El problema es que las colas globales son colas concurrentes, por lo que no está logrando la sincronización que desea. Pero si creaste tu propia cola serie, eso hubiera estado bien, por ejemplo, en Swift 3:

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

Esta cola personalizada es, por defecto, una cola en serie. Por lo tanto, logrará la sincronización que deseaba.

Tenga en cuenta que si utiliza este blocksDispatchQueue para sincronizar su interacción con esta cola, toda la interacción con este blocksArray debe coordinarse a través de esta cola, por ejemplo, también despache el código para agregar las operaciones utilizando la misma cola:

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

Alternativamente, también puede emplear el patrón de lector / escritor , creando su propia cola simultánea:

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

Pero en el patrón de lector-escritor, las escrituras deben realizarse utilizando barreras (logrando un comportamiento tipo seriado para las escrituras):

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

Pero ahora puedes leer datos, como se muestra arriba:

 blocksDispatchQueue.sync { someVariable = self.blocksArray[index] } 

El beneficio de este patrón es que las escrituras están sincronizadas, pero las lecturas pueden ocurrir al mismo tiempo entre sí. Probablemente esto no sea crítico en este caso (por lo que una simple cola en serie probablemente sea suficiente), pero incluyo este patrón de lectura y escritura para completar.

Si está buscando ejemplos de Swift 2, vea la versión anterior de esta respuesta.

Para la sincronización entre hilos, use dispatch_sync (no _async) y su propia cola de despacho (no la global):

 class MyArrayBlockClass { private var queue = dispatch_queue_create("andrew.myblockarrayclass", nil) func addBlockToArray(block: () -> Void) { dispatch_sync(queue) { self.blocksArray.append(block) } } //.... } 

dispatch_sync es agradable y fácil de usar, y debería ser suficiente para su caso (lo uso para todas las necesidades de sincronización de subprocesos en este momento), pero también puede usar lockings de bajo nivel y mutexes. Hay un gran artículo de Mike Ash sobre diferentes opciones: lockings, seguridad de subprocesos y Swift

Cree una cola en serie y realice cambios en la matriz en esa secuencia. Su llamada de creación de hilo debería ser algo como esto

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

Entonces puedes usarlo de la misma manera que en este momento.

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

NSOperationQueue sí mismo es seguro para subprocesos, por lo que puede configurar suspended en verdadero, agregar todos los bloques que desee de cualquier subproceso y luego establecer suspended en falso para ejecutar todos los bloques.

Detalles

xCode 9.1, Swift 4

Solución

 import Foundation class AtomicArray: CustomStringConvertible { private let semaphore = DispatchSemaphore(value: 1) private var semaphors = [DispatchSemaphore]() private var array: [T] func append(newElement: T) { sync { array.append(newElement) } } subscript(index: Int) -> T { get { wait() defer { signal() } return array[index] } set(newValue) { wait() array[index] = newValue signal() } } 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]) ) { sync { array = closure(array) } } func get(closure: (_ curentArray: [T])->()) { wait() closure(array) signal() } func get() -> [T] { wait() defer { signal() } return array } private func sync (closure:()->()) { wait() closure() signal() } init (array: [T]) { self.array = array } var description: String { wait() defer { signal() } return "\(array)" } } 

Uso

La idea básica es usar la syntax de una matriz regular

 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) } 

Resultado de uso

enter image description here

Muestra completa

 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.. 

Resultado de muestra completo

enter image description here