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
相关产品推荐
相关产品推荐

