当数组在Swift中具有并发读取时,如何实现removeAtIndex的线程安全?

当数组在Swift中具有并发读取时,如何实现removeAtIndex的线程安全?,swift,multithreading,grand-central-dispatch,race-condition,Swift,Multithreading,Grand Central Dispatch,Race Condition,我在看一个为线程安全数组提供代码的并发读取。正如@tombardey在评论中指出的,代码(下面的相关代码片段)并非完全安全: public func removeAtIndex(index: Int) { self.accessQueue.async(flags:.barrier) { self.array.remove(at: index) } } public var count: Int { var count = 0 self.acc

我在看一个为线程安全数组提供代码的并发读取。正如@tombardey在评论中指出的,代码(下面的相关代码片段)并非完全安全:

public func removeAtIndex(index: Int) {

    self.accessQueue.async(flags:.barrier) {
        self.array.remove(at: index)
    }
}

public var count: Int {
    var count = 0

    self.accessQueue.sync {
        count = self.array.count
    }

    return count
}
…假设同步化数组 一个因素,这不会失败吗?如果synchronizedArray.count==1{ synchronizedArray.remove(at:0)}这是一个竞争条件,比如说两个 线程执行该语句。两者同时读取计数1, 二者同时将写入块排队。执行写块 按顺序,第二个将失败。。。(续)

@罗布答复:

@汤姆巴迪-你完全正确,这一级别的 同步(在属性/方法级别)经常被忽略 不足以在更广泛的应用中实现真正的线程安全。 您的示例很容易解决(通过添加分派的方法) 阻止进入队列),但也有其他人没有(例如。 UITableViewDataSource同时使用的“同步”数组 并通过一些后台操作进行了变异)。在这种情况下,你必须 实现您自己的高级同步。但是上面 尽管如此,该技术在某些高度受限的情况下仍然非常有用 情况


我很难理解@Rob所说的“您的示例很容易解决(通过添加一个将块分派到队列的方法)”,这意味着什么。我希望看到此方法(或任何其他)技术的示例实现来解决此问题。

我看到发布的代码和示例存在一些问题:

  • 函数
    removeAtIndex
    没有检查它是否可以在提供的索引中实际删除。所以应该改成

    public func removeAtIndex(index: Int) {
    
        // Check if it even makes sense to schedule an update
        // This is optional, but IMO just a better practice
        guard count > index else { return }
    
        self.accessQueue.async(flags: .barrier) {
    
            // Check again before removing to make sure array didn't change
            // Here we can actually check the size of the array, since other threads are blocked
            guard self.array.count > index else { return }
            self.array.remove(at: index)
        }
    }
    
  • 使用线程安全类还意味着您使用一个操作来检查和执行一个应该是线程安全的项上的操作。所以,如果您检查数组大小,然后删除它,您就是在破坏线程安全封套,这不是该类的正确使用。特殊情况
    synchronizedArray.count==1{synchronizedArray.remove(at:0)}
    通过对上述函数的调整得到解决(您不再需要检查count,因为函数已经这样做了)。但是,如果您仍然需要一个同时执行这两个操作、验证计数并删除一个项的函数,则必须在线程安全类中创建一个函数,该函数执行这两个操作,而其他线程不可能在这两个操作之间修改数组。您甚至可能需要两个函数:
    synchronizedArray.getCountAndRemove
    (获取计数,然后删除)
    ,以及
    synchronizedArray.removeAndGetCount`(删除,然后获取计数)

  • 一般来说,从多个线程中删除一个数组的索引项是毫无意义的。你甚至不能确定要删除什么。也许在某些情况下它是有意义的,但通常通过某种逻辑(例如特定值)删除,或者使用一个函数返回它删除的项的值(例如
    func getandremoveatinex(index:Int)->T

  • 始终测试每个功能及其组合。例如,如果原始海报测试如下移除:

    let array = SynchronizedArray<Int>()
    array.append(newElement: 1)
    array.append(newElement: 2)
    array.append(newElement: 3)
    
    DispatchQueue.concurrentPerform(iterations: 5) {_ in
    
        array.removeAtIndex(index: 0)
    }
    
    let array=SynchronizedArray()
    array.append(新元素:1)
    array.append(新元素:2)
    array.append(新元素:3)
    DispatchQueue.concurrentPerform(迭代次数:5){in
    array.removatendex(索引:0)
    }
    
  • 他将在5个线程中的2个线程中得到一个
    致命错误:索引超出范围:文件Swift/Array.Swift,第1298行
    ,因此很明显,此函数的原始实现是不正确的。对我上面发布的函数尝试相同的测试,您将看到不同之处


    顺便说一句,我们只讨论了
    removatendex
    ,但是
    subscript
    也有类似的问题。但有趣的是,
    first()
    的实现是正确的。

    我发现发布的代码和示例存在一些问题:

  • 函数
    removeAtIndex
    没有检查它是否可以在提供的索引中实际删除。所以应该改成

    public func removeAtIndex(index: Int) {
    
        // Check if it even makes sense to schedule an update
        // This is optional, but IMO just a better practice
        guard count > index else { return }
    
        self.accessQueue.async(flags: .barrier) {
    
            // Check again before removing to make sure array didn't change
            // Here we can actually check the size of the array, since other threads are blocked
            guard self.array.count > index else { return }
            self.array.remove(at: index)
        }
    }
    
  • 使用线程安全类还意味着您使用一个操作来检查和执行一个应该是线程安全的项上的操作。所以,如果您检查数组大小,然后删除它,您就是在破坏线程安全封套,这不是该类的正确使用。特殊情况
    synchronizedArray.count==1{synchronizedArray.remove(at:0)}
    通过对上述函数的调整得到解决(您不再需要检查count,因为函数已经这样做了)。但是,如果您仍然需要一个同时执行这两个操作、验证计数并删除一个项的函数,则必须在线程安全类中创建一个函数,该函数执行这两个操作,而其他线程不可能在这两个操作之间修改数组。您甚至可能需要两个函数:
    synchronizedArray.getCountAndRemove
    (获取计数,然后删除)
    ,以及
    synchronizedArray.removeAndGetCount`(删除,然后获取计数)

  • 一般来说,从多个线程中删除一个数组的索引项是毫无意义的。你甚至不能确定要删除什么。也许在某些情况下它是有意义的,但通常通过某种逻辑(例如特定值)删除,或者使用一个函数返回它删除的项的值(例如
    func getandremoveatinex(index:Int)->T

  • 始终测试每个功能及其组合。例如,如果原始海报测试如下移除:

    let array = SynchronizedArray<Int>()
    array.append(newElement: 1)
    array.append(newElement: 2)
    array.append(newElement: 3)
    
    DispatchQueue.concurrentPerform(iterations: 5) {_ in
    
        array.removeAtIndex(index: 0)
    }
    
    let array=SynchronizedArray()
    array.append(新元素:1)
    array.append(新元素:2)
    array.append(新元素:3)
    DispatchQueue.concurrentPerform(迭代次数:5){in
    array.removatendex(索引:0)
    }
    
  • 他将得到一个
    致命错误:索引超出范围:文件Swift/Array.Swift,第1298行
    
    numbers.safelyRemove(at: index)