导读:本期聚焦于上海GEO公司创作的《Swift中的Sendable类与Actor如何隔离引用类型并防止数据竞争?》,敬请观看详情。一个可变引用类型实例被多个并发任务同时修改时,哪些操作会被编译器拦下,哪些又可能悄悄穿透检查?Swift的Sendable协议与Actor模型分别从类型约束和执行边界两个层面给出答案。Sendable要求跨并发域传递的引用类型具备不可变语义或显式安全承诺,否则编译器会拒绝潜在危险代码。Actor则把内部引用类型状态收进串行执行队列,外部只能通过异步方法访问,从根源上避免同一时刻的多线程写入。本文结合final class、let属性、@unchecked Sendable等具体写法,分析引用类型在Swift并发环境中的隔离策略,并给出Sendable类与Actor组合使用时的防数据竞争方案。

在Swift并发编程里,引用类型是最容易被忽视的竞争来源。值类型由于拷贝语义,跨任务传递时通常天然安全;但一个class实例的引用被多个任务同时持有时,任何对内部var属性的修改都可能产生不确定结果。Swift 5.5之后引入的Sendable标记和Actor执行体,正是针对这类问题的两层防线。前者告诉编译器哪些类型可以安全地跨越并发域,后者创建了一个串行化的状态容器。理解二者如何作用于引用类型,可以帮助在实际工程中把数据竞争挡在编译期。

Swift中的Sendable类与Actor如何隔离引用类型并防止数据竞争?

Sendable协议如何约束引用类型

Sendable协议本身没有方法要求,它是一个标记协议,表示该类型的实例可以被安全地从一个并发域传递到另一个并发域。对于枚举和结构体,只要所有存储属性都是Sendable,编译器可以自动推导;但引用类型想要遵循Sendable,条件要严格得多。编译器允许class遵循Sendable的前提是:它必须是final class,并且所有存储属性都是不可变的let常量,而且这些属性的类型本身也必须是Sendable。这样设计的原因是,只有不可变引用类型才能保证多个任务同时读取同一实例时不会发生写入竞争。

例如下面这个UserProfile类就可以安全地标记为Sendable,因为它没有可变状态,任何任务拿到引用后只能读取,不能修改:

final class UserProfile: Sendable {
    let userID: Int
    let nickname: String

    init(userID: Int, nickname: String) {
        self.userID = userID
        self.nickname = nickname
    }
}

如果去掉final声明,或者把某个let改成var,编译器会立即报告错误,提示该class不符合Sendable要求。这种编译期检查非常有用,它防止了团队在后续维护中不小心给本应不可变的对象添加可变状态。需要注意的是,编译器对NSObject子类的检查并不完全可靠,如果一定要跨线程传递可变OC对象,就需要谨慎使用@unchecked Sendable,但这意味着开发者自己承担全部线程安全责任,应当尽量避免。

还有一类情况是引用类型内部虽然可变,但外部访问全部通过锁或串行队列保护。此时理论上它是线程安全的,但编译器无法自动证明,只能通过@unchecked Sendable手动承诺。不过这种写法绕过了编译器的安全网,一旦锁的粒度或使用方式出错,数据竞争仍然会发生。因此对于大多数业务模型,更好的做法是让类本身保持不可变,把可变状态交给Actor管理。

Actor如何为引用类型建立隔离边界

Actor是Swift并发模型中的引用类型,但它与普通class最大的区别在于:Actor内部的状态在运行时被串行访问。每个Actor实例拥有独立的执行上下文,对它的所有方法调用都必须通过异步机制进入,编译器会强制使用await。这样一来,即使Actor内部存储了一个可变的class实例,外部任务也不能直接触碰那个class的属性,只能调用Actor暴露的方法,由Actor在自己的串行上下文中完成读取或修改。

下面这个例子中,SessionManager是一个Actor,它内部持有一个可变引用类型SessionState。外部并发任务不能直接修改state,只能通过updateLastAccess这类Actor方法操作,从而避免了多线程同时写入:

final class SessionState {
    var lastAccessTime: Date
    var activeCount: Int

    init(lastAccessTime: Date, activeCount: Int) {
        self.lastAccessTime = lastAccessTime
        self.activeCount = activeCount
    }
}

actor SessionManager {
    private let state: SessionState

    init(initialState: SessionState) {
        self.state = initialState
    }

    func recordAccess() {
        state.lastAccessTime = Date()
        state.activeCount += 1
    }

    func currentState() -> SessionState {
        return state
    }
}

注意currentState返回SessionState是一个类引用,这会把内部可变引用暴露给外部。如果调用方拿到引用后在其他线程修改它,隔离就被破坏了。因此更安全的做法是让SessionState遵循Sendable且不可变,或者让Actor返回一份值类型快照。这个细节说明,Actor的隔离边界并不是自动延伸到其内部持有的引用类型上的,必须配合Sendable设计才能形成完整防线。

另一个容易混淆的点是,Actor方法内部可以同步访问自己的状态,但外部调用必须await。这类似于把每个Actor实例看成一个串行队列,所有方法调用都排队执行。虽然它避免了低层次锁的显式使用,但也要注意不要在Actor方法中执行长耗时同步操作,否则会阻塞该Actor的串行上下文,影响后续任务排队。

Sendable类与Actor的组合策略

在实际架构中,把Sendable类和Actor结合起来,可以同时获得不可变数据的自由传递能力和可变状态的集中管理能力。一种常见模式是:用Sendable值类型或不可变引用类型作为跨任务传输的数据包,用Actor作为唯一可变状态的拥有者。例如,网络层先请求到一个DTO,这个DTO是Sendable的,可以安全地从后台任务传回主Actor;而写入缓存、更新列表等可变操作则全部交给一个DataStore Actor完成。

下面的代码展示了一个简单组合:FeedItem是不可变Sendable类,FeedStore Actor负责持有并修改数组。外部任务可以安全地传入FeedItem实例,但无法直接改动存储内容:

final class FeedItem: Sendable {
    let id: UUID
    let title: String

    init(id: UUID, title: String) {
        self.id = id
        self.title = title
    }
}

actor FeedStore {
    private var items: [FeedItem] = []

    func insert(_ item: FeedItem) {
        items.append(item)
    }

    func allItems() -> [FeedItem] {
        return items
    }
}

FeedItem虽然是class,但因为final且只有let属性,它满足Sendable,可以被安全地传入Actor。而FeedStore内部的数组虽然可变,却被Actor隔离,外部无法直接操作。这样即使多个任务同时向同一个FeedStore插入数据,实际写入操作也会被串行化,不会出现数组底层存储被并发修改的问题。

另一种组合方式是把Actor方法中的参数和返回值都约束为Sendable。Swift编译器会检查跨Actor调用时传递的类型是否满足Sendable,如果不满足会直接报错。这个检查在Swift 6模式下会变得更严格,Swift 5语言模式下很多场景只是警告,但建议从现在开始就把公开API设计为Sendable友好的。特别是涉及闭包时,例如Task.detached中捕获的引用类型,如果捕获了非Sendable的class实例,就可能导致隐性竞争。

还需要注意@preconcurrency importnonisolated等语法对兼容性的影响。例如某些系统框架的类在旧SDK中未标记Sendable,升级并发检查后可能产生大量警告。此时不应立刻大面积添加@unchecked Sendable,而应分析每个类型是否真的不可变或是否有Actor保护。只有通过组合策略把可变状态集中在少数Actor里,把跨任务传递的数据尽量设计成不可变Sendable类或值类型,才能让编译器检查真正发挥作用,而不是靠禁用并发检查来消除报错。

总体来看,Swift对引用类型并发安全的处理思路很清晰:Sendable负责回答类型能不能安全传递,Actor负责回答可变状态在哪里被修改。单靠其中任何一个都不完整,只有将不可变引用类型与Actor隔离结合起来,才能在保留class引用语义的同时有效预防数据竞争。

Swift并发数据竞争Actor隔离修改时间:2026-08-24 11:19:43

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。