Swift 中的单元测试线程安全列表

nat*_*ft1 4 multithreading ios swift

我有一个 AtomicList 类型:

public struct AtomicList<Element> {
    public var values: [Element] = []
    private let queue = DispatchQueue(label: UUID().uuidString)
    
    public var last: Element? {
        self.queue.sync { return self.values.last }
    }
    
    public mutating func append(item: Element) {
        self.queue.sync {
            self.values.append(item)
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

还有一个不稳定的失败测试,​​应该验证它是线程安全的:

class AtomicListTests: XCTestCase {
    func testReadingAndWritingOnDifferentThreads() {
        var arrayA = AtomicList<Int>()
        var arrayB = AtomicList<Int>()
        
        let expectation = expectation(description: "thread safety test")
        expectation.expectedFulfillmentCount = 10000
        
        for i in 0..<expectation.expectedFulfillmentCount {
            DispatchQueue.global().async {
                arrayA.append(item: i)
                if let last = arrayA.last {
                    arrayB.append(item: last)
                }
                expectation.fulfill()
            }
        }
        
        wait(for: [expectation], timeout: 5.0)
        XCTAssertEqual(arrayA.values.count, arrayB.values.count)
    }
}
Run Code Online (Sandbox Code Playgroud)

我有两个问题:

  1. 这是否是检查该类型是否线程安全的有效测试?
  2. 为什么测试偶尔会失败,并出现 9999 不等于 10000 的错误(即计数不匹配)

注意:我注意到有几个解决方案可以解决此问题并使其通过测试,例如:

  1. 将 AtomicList 从结构更改为类
  2. 将 var last 更改为 mutating func
  3. 测试中同步访问arrayA和arrayB

但我仍然不完全理解为什么现有代码不起作用,以及为什么上面的前两个解决方案使它通过。

Rob*_*ier 5

这是否是检查该类型是否线程安全的有效测试?

不会。仅仅因为竞争条件未能导致症状并不意味着不存在竞争条件。要测试测试期间发生的基本内存竞争条件,您需要使用线程清理程序。这会检测您的代码,以确保每个跨线程内存访问都受到锁的保护。这仍然不能证明不存在低级竞争条件,因为它可能不会执行所有代码路径(尽管它会检测不受保护的访问,即使它们碰巧没有冲突,这就是它的要点)。

所有这些仍然并不意味着没有更高级别的竞争条件。请参阅 bbum 的回答,了解为什么 Objective-Catomic不提供高级线程安全性,即使它消除了所有低级数据竞争。以这种方式证明系统是线程安全的可能是不可能的。

对于为什么此代码不起作用的问题,这不是访问值类型的有效方法。值类型在传递给函数或变异时会被复制。它们是价值观。它们不是一个可以有多个引用指向同一个引用的对象。从多个线程访问同一结构不是定义的行为,并且破坏了写时复制优化。您在内部数组周围有一个锁,但在 AtomicList 结构周围没有锁。(如果编译器在此方面失败就好了,但它会破坏太多进行单个结构修改的常见 GCD 模式。)

这意味着对内部数组的访问是同步的,但对结构体的修改不是同步的。你的问题比偶尔的长度不匹配要大得多。价值观也是错误的。如果我运行 100 次并检查arrayB,前几个元素通常是错误的。有时是[1,1,2],有时是[1,0,2],等等。

正确的工具是actor. 但对于 GCD 来说,这仅对引用类型的类有意义,因此锁可以工作。