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

Swift Actor引用非Sendable类ID的并发安全疑问

关于Actor与可变引用类型的竞态条件问题分析

首先看你提供的代码:

class ID: Hashable {
    var id: Int
    
    init(id: Int) {
        self.id = id
    }
    
    static func == (lhs: ID, rhs: ID) -> Bool {
        lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

actor LibraryAccount {
    let id: ID
    var booksOnLoan: [Book]
    
    init(id: ID, booksOnLoan: [Book]) {
        self.id = id
        self.booksOnLoan = booksOnLoan
    }
}

extension LibraryAccount: Equatable {
    static func == (lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
        lhs.id == rhs.id
    }
}

extension LibraryAccount: Hashable {
    nonisolated func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

为什么严格并发检查下无警告?

  • Actor的id是let属性,编译器允许在非隔离上下文获取这个引用类型的实例——因为引用本身不可变,不会违反Actor的隔离规则(Actor的存储属性引用无法被外部修改)。但这并不代表ID实例是线程安全的,严格并发检查只管控Actor自身状态的访问,不会强制要求外部引用类型遵循Sendable,只要你没在隔离域外修改Actor的存储属性(这里id是let,做不到),编译器就不会报错。

你观察到的竞态条件确实存在

当调用LibraryAccount的==方法时,lhs.id == rhs.id会触发两次对ID.id属性的读取(分别读取两个ID实例的id值)。这两次读取之间,完全可能有其他线程修改其中一个或两个ID的id属性,导致比较结果为true,但实际两个ID的id已经不一致——这是典型的非线程安全引用类型带来的竞态问题。

你忽略的核心关键点

Actor的隔离机制只保护自身的存储属性(这里是id引用本身和booksOnLoan数组),但不负责管控引用指向的外部对象的状态。ID作为可变、非线程安全的类,它的状态修改不受Actor隔离的约束,任何线程都可以直接修改ID.id,这才是问题的根源。

修复思路(可选)

  • 将ID改为值类型(struct):值语义会保证比较时的原子性,读取的是当前值的副本,避免竞态。
  • 让ID遵循Sendable并保证线程安全:比如把ID改成actor,或者用锁/原子属性保护id的读写。
  • 调整LibraryAccount的比较逻辑:在Actor隔离域内读取ID.id的值(比如添加async方法获取id值),再基于这些值做比较——不过Equatable协议不支持async,所以需要重新设计比较方式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 15:25:27