Добавление элементов в массив Swift для нескольких потоков, вызывающих проблемы (поскольку массивы не являются потокобезопасными) - как мне обойти это?
Я хочу добавить заданные блоки в массив, а затем запустить все блоки, содержащиеся в массиве, по запросу. У меня есть код, похожий на этот:
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)
}
}
Проблема заключается в том, что addBlockToArray можно вызывать через несколько потоков. То, что происходит, - addBlockToArray вызывается быстро в разных потоках и только добавляет один из элементов, и поэтому другой элемент не получает вызов во время runBlocksInArray.
Я пробовал что-то вроде этого, которое, похоже, не работает:
private let blocksDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
func addBlockToArray(block: () -> Void) {
dispatch_async(blocksDispatchQueue) {
self.blocksArray.append(block)
}
}
Ответы
Ответ 1
Вы определили blocksDispatchQueue
как глобальную очередь. Обновление для Swift 3 эквивалентно:
private let queue = DispatchQueue.global()
func addBlockToArray(block: @escaping () -> Void) {
queue.async {
self.blocksArray.append(block)
}
}
Проблема в том, что глобальные очереди - это параллельные очереди, поэтому вы не достигнете желаемой синхронизации. Но если бы вы создали свою собственную последовательную очередь, это было бы хорошо, например, в Swift 3:
private let queue = DispatchQueue(label: "com.domain.app.blocks")
Эта пользовательская очередь по умолчанию является последовательной очередью. Таким образом, вы достигнете желаемой синхронизации.
Обратите внимание, что если вы используете этот blocksDispatchQueue
для синхронизации вашего взаимодействия с этой очередью, все взаимодействия с этим blocksArray
должны координироваться через эту очередь, например, также отправьте код для добавления операций с использованием той же очереди:
func runBlocksInArray() {
queue.async {
for block in self.blocksArray {
let operation = BlockOperation(block: block)
self.blocksQueue.addOperation(operation)
}
self.blocksArray.removeAll()
}
}
В качестве альтернативы вы также можете использовать шаблон чтения/записи, создавая собственную параллельную очередь:
private let queue = DispatchQueue(label: "com.domain.app.blocks", attributes: .concurrent)
Но в шаблоне "читатель-писатель" записи должны выполняться с использованием барьера (достижение последовательного поведения для записей):
func addBlockToArray(block: @escaping () -> Void) {
queue.async(flags: .barrier) {
self.blocksArray.append(block)
}
}
Но теперь вы можете читать данные, как указано выше:
let foo = queue.sync {
blocksArray[index]
}
Преимущество этого шаблона заключается в том, что записи синхронизируются, но чтения могут происходить одновременно по отношению друг к другу. Это, вероятно, не критично в этом случае (так что простой последовательной очереди, вероятно, будет достаточно), но для полноты картины я включил этот шаблон чтения-записи.
Другой подход - NSLock
:
extension NSLocking {
func withCriticalSection<T>(_ closure: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try closure()
}
}
И таким образом:
let lock = NSLock()
func addBlockToArray(block: @escaping () -> Void) {
lock.withCriticalSection {
blocksArray.append(block)
}
}
Но теперь вы можете читать данные, как указано выше:
let foo = lock.withCriticalSection {
blocksArray[index]
}
Исторически NSLock
всегда считался менее производительным, но в настоящее время это даже быстрее, чем GCD.
Если вы ищете примеры Swift 2, посмотрите предыдущее исполнение этого ответа.
Ответ 2
Для синхронизации между потоками используйте dispatch_sync
(not _async) и свою собственную очередь отправки (а не глобальную):
class MyArrayBlockClass {
private var queue = dispatch_queue_create("andrew.myblockarrayclass", nil)
func addBlockToArray(block: () -> Void) {
dispatch_sync(queue) {
self.blocksArray.append(block)
}
}
//....
}
dispatch_sync
приятный и простой в использовании и должен быть достаточным для вашего случая (я использую его для всех потребностей синхронизации потоков в данный момент), но вы также можете использовать блокировки и мьютексы нижнего уровня. Есть замечательная статья Майка Эша о разных вариантах: Замки, безопасность потоков и скорость
Ответ 3
Создайте очередную очередь и внесите изменения в массив в этом потоке.
Ваш вызов создания потока должен быть чем-то вроде этого
private let blocksDispatchQueue = dispatch_queue_create("SynchronizedArrayAccess", DISPATCH_QUEUE_SERIAL)
Затем вы можете использовать его так же, как сейчас.
func addBlockToArray(block: () -> Void) {
dispatch_async(blocksDispatchQueue) {
self.blocksArray.append(block)
}
}
Ответ 4
NSOperationQueue
сам по себе является потокобезопасным, поэтому вы можете установить suspended
в true, добавить все блоки, которые вы хотите из любого потока, а затем установить suspended
в false, чтобы запустить все блоки.
Ответ 5
подробности
- Xcode 10.1 (10B61)
- Swift 4.2
Решение
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())"}
}
использование
Основная идея заключается в использовании синтаксиса регулярного массива
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)
Результат использования
![enter image description here]()
Полный образец
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()
}
}
}
}
Полный образец результата
![enter image description here]()