跨多个线程向 Swift 数组添加项目会导致问题(因为数组不是线程安全的) - 我该如何解决这个问题?

问题描述 投票:0回答:5

我想将给定的块添加到数组中,然后根据请求运行数组中包含的所有块。我有类似的代码:

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)
    }
}
arrays swift multithreading grand-central-dispatch read-write
5个回答
42
投票

您已将

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
all
交互应通过此队列进行协调,例如还调度代码以使用同一队列添加操作:

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
withLock
,它会为你平衡
lock
unlock

let lock = NSLock()

func addBlockToArray(block: @escaping () -> Void) {
    lock.withLock {
        blocksArray.append(block)
    }
}

但是您现在可以读取数据,如上所示:

let foo = lock.withCriticalSection {
    blocksArray[index]
}

历史上

NSLock
被认为性能较差而被忽视,但现在它甚至比 GCD 更快。


如果您正在寻找 Swift 2 示例,请参阅此答案的之前的版本


3
投票

对于线程之间的同步,请使用

dispatch_sync
(不是 _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
很好而且易于使用,应该足以满足您的情况(我目前使用它来满足所有线程同步需求),但您也可以使用较低级别的锁和互斥锁。 Mike Ash 有一篇很棒的文章,介绍了不同的选择:锁、线程安全和 Swift


1
投票

创建一个串行队列并在该线程中更改数组。 你的线程创建调用应该是这样的

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

然后你就可以像现在一样使用它了。

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

1
投票

详情

  • Xcode 10.1 (10B61)
  • 斯威夫特 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


0
投票

NSOperationQueue
本身是线程安全的,因此您可以将
suspended
设置为 true,从任何线程添加所需的所有块,然后将
suspended
设置为 false 以运行所有块。

© www.soinside.com 2019 - 2024. All rights reserved.