Добавление элементов в массив 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