Swift中AtomicList线程安全测试有效性及失效原因咨询
关于AtomicList线程安全测试的问题
我定义了如下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) } } }
同时编写了一个验证其线程安全的测试,但该测试偶尔会失败:
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) } }
我有两个问题:
- 该测试是否能有效验证AtomicList的线程安全性?
- 为何测试偶尔会出现计数不匹配(9999不等于10000)的失败?
注:我发现三种可修复该测试的方案:
- 将AtomicList从结构体改为类
- 将last属性改为mutating方法
- 在测试中同步对arrayA和arrayB的访问
但我仍不理解现有代码失效的原因,以及前两种方案能解决问题的原理。
问题解答
1. 该测试是否能有效验证AtomicList的线程安全性?
这个测试不能有效验证AtomicList的线程安全性。原因在于测试逻辑本身存在竞态条件:arrayA.append(item: i)和arrayA.last是两个独立的原子操作,但整个"写入后立即读取最后一个元素"的流程没有同步机制。即便AtomicList的单个方法是线程安全的,测试的组合操作也引入了外部竞态,无法准确反映AtomicList自身的线程安全特性。
2. 为何测试偶尔会出现计数不匹配?
核心问题在于AtomicList是值类型(结构体):
- 调用
append(mutating方法)时,会生成arrayA的副本,修改副本后再赋值回原变量。多线程环境下,多个线程同时修改arrayA会出现副本覆盖:比如线程1刚生成arrayA副本准备修改,线程2已经完成修改并把新副本赋值回去,此时线程1修改后的副本会覆盖线程2的修改,导致arrayA.values计数少一次。 last是非mutating属性,访问时会读取当前arrayA的副本,可能拿到的是旧版本的arrayA,此时last为nil,导致arrayB没有追加元素,最终两个计数不匹配。
前两种修复方案的原理
改为类(引用类型):
类是引用类型,所有线程操作的都是同一个实例的引用,不会像结构体那样产生副本。调用append时不会生成新实例,所有线程对arrayA的修改都作用在同一个对象上,避免了副本覆盖问题,同时last访问的也是最新的实例状态。将last属性改为mutating方法:
mutating方法会强制调用者为var变量,且调用时会获取结构体的写权限(生成副本并替换原变量)。这样last的访问和append一样,都会触发结构体的副本更新机制,确保每次访问last时拿到的是最新的arrayA状态,避免读取旧副本导致的nil情况,同时减少了多线程下副本覆盖的概率。
内容的提问来源于stack exchange,提问作者natecraft1
相关产品推荐
相关产品推荐

