You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)
    }
}

我有两个问题:

  1. 该测试是否能有效验证AtomicList的线程安全性?
  2. 为何测试偶尔会出现计数不匹配(9999不等于10000)的失败?

注:我发现三种可修复该测试的方案:

  1. 将AtomicList从结构体改为类
  2. 将last属性改为mutating方法
  3. 在测试中同步对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没有追加元素,最终两个计数不匹配。

前两种修复方案的原理

  1. 改为类(引用类型):
    类是引用类型,所有线程操作的都是同一个实例的引用,不会像结构体那样产生副本。调用append时不会生成新实例,所有线程对arrayA的修改都作用在同一个对象上,避免了副本覆盖问题,同时last访问的也是最新的实例状态。

  2. 将last属性改为mutating方法:
    mutating方法会强制调用者为var变量,且调用时会获取结构体的写权限(生成副本并替换原变量)。这样last的访问和append一样,都会触发结构体的副本更新机制,确保每次访问last时拿到的是最新的arrayA状态,避免读取旧副本导致的nil情况,同时减少了多线程下副本覆盖的概率。


内容的提问来源于stack exchange,提问作者natecraft1

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 20:57:07