Sendable 不是“能跑就行”:Swift Concurrency 下的实战踩坑与架构落地指南

面试里聊 Sendable,最容易被一句话带偏:"我不写 : Sendable 也能跑啊。"

架构师视角下,这句话等价于:"我没开编译器安全带,所以也没撞见气囊。"


1. Sendable 在并发生态里的"生态位"

Swift Concurrency 体系里,Sendable 的职责非常明确:

  • async/await:描述异步控制流
  • Task/TaskGroup:组织并发任务
  • Actor/MainActor:隔离共享状态访问
  • Sendable:约束"跨并发域传值是否安全"

一句话Sendable 是跨隔离域/跨线程传递数据的"静态安全证明"。

它不负责让你的代码"跑起来",它负责让你的代码"长期可维护且不出并发事故"。


2. 坑 1:不写 Sendable 也能打印日志,误以为"编译器不管"

很多人第一次写 detached:

swift 复制代码
struct User {
    let id: Int
    let name: String
}

func sendToBg(_ user: User) {
    Task.detached {
        print("后台收到:\(user.name)")
    }
}

结果能跑、能打印,于是得出结论:Sendable 可有可无。

架构师解读

  • Sendable 是编译期约束,不是运行期机制。
  • 你没看到报错,可能只是项目的 Strict Concurrency Checking 不够严格(或 Swift 语言模式不是 6)。
  • 更重要的是:即使它在当前设置下没报,团队协作、编译器升级、跨模块演进后,这类"不显式表达并发意图"的代码会非常脆弱。

工程建议(DTO 约定)

swift 复制代码
struct User: Sendable {
    let id: Int
    let name: String
}

3. 坑 2:值类型也会"看起来像共享"------闭包捕获语义反直觉

你以为 struct 是值语义,所以后台任务应该拿到"创建时的值",但你写:

swift 复制代码
struct Teacher: Sendable { var name: String }

func test() {
    var t = Teacher(name: "Oliver")

    Task.detached {
        try? await Task.sleep(nanoseconds: 200_000_000)
        print("后台读取:\(t.name)")
    }

    t.name = "Jack"
    print("主线程修改:\(t.name)")
}

你很可能看到后台打印的是 Jack

根因

  • 不写捕获列表时,闭包捕获的是外层 var t 这个"变量位置",而不是你心里期待的"值快照"。
  • 这会让"跨域传值"的语义变得不清晰:你到底是传"快照",还是传"后续可能变化的变量"?

正确演进:显式快照

swift 复制代码
let snapshot = t
Task.detached {
    print("后台读取:\(snapshot.name)")
}

或者:

swift 复制代码
Task.detached { [t] in
    print("后台读取:\(t.name)")
}

架构落地规则

  • "跨并发域传值"统一使用 不可变快照(let + struct),避免让捕获语义隐式决定数据版本。

4. 坑 3:final class: Sendable 在 Swift 6 会直接"教做人"

很多同学自然会写:

swift 复制代码
final class Profile: Sendable {
    var nickName: String
    init(nickName: String) { self.nickName = nickName }
}

Swift 6 直接报(或升级为 error):

  • Stored property 'nickName' of 'Sendable'-conforming class 'Profile' is mutable

根因

  • class 是引用语义:跨并发域传的是同一个对象地址。
  • var 是共享可变状态:多个并发任务可能同时读写。
  • 你让它 Sendable,等于向编译器承诺"它能安全跨域共享",但你没有任何隔离/同步策略。

正确演进 1:不可变引用对象

swift 复制代码
final class Profile: Sendable {
    let nickName: String
    init(nickName: String) { self.nickName = nickName }
}

这类对象可以安全跨域共享:因为它不可变。


5. 坑 4:@unchecked Sendable 不是解决方案,是"关闭报警器"

当你写:

swift 复制代码
final class Profile: @unchecked Sendable {
    var nickName: String
    init(nickName: String) { self.nickName = nickName }
}

编译器不拦你了,但你获得的不是安全,而是"责任自负"。

很容易写出这种并发读写:

swift 复制代码
Task.detached { user.profile.nickName = "后台改名1" }
Task.detached { print(user.profile.nickName) }

你会看到输出顺序与读取值不稳定,这不是"多线程正常现象",这是共享可变引用状态的典型风险:data race 可能发生,只是未必崩在你眼前

架构师原则

  • @unchecked Sendable 只允许出现在你能明确解释同步策略的地方(锁/串行队列/actor/mainactor),并且要极少、可审计。

6. 正确建模:共享可变状态就用 actor,不要硬把 class 送上 Sendable

如果 Profile 本质上就是"会变的共享状态",它的正确生态位是 actor

swift 复制代码
actor Profile {
    private var nickName: String
    init(nickName: String) { self.nickName = nickName }

    func updateName(_ newName: String) {
        nickName = newName
    }

    func currentName() -> String {
        nickName
    }
}

外部必须通过 await 访问:

swift 复制代码
Task.detached {
    await user.profile.updateName("后台改名1")
    let name = await user.profile.currentName()
    print("后台读取:\(name)")
}

注意:你仍然会看到读写交错(因为任务调度非确定),但这是在 actor 隔离下的"合法交错",不是裸奔式共享内存并发读写。


7. 架构落地:一套团队可执行的规则

  • 跨线程传值对象(DTO/参数/响应) :优先 struct: Sendable,尽量 let;需要变化时用"生成新值",不要原地改。
  • 共享可变状态(缓存/账户/会话/库存) :优先 actor 建模,把并发边界收敛到隔离域。
  • final class: Sendable :只用于不可变引用对象(全 let)。
  • @unchecked Sendable:仅用于你能证明线程安全的历史类,并收敛到极少数点,禁止泛滥。

8. 结语:Sendable 的价值不在"现在能跑",在"未来敢改"

架构师看并发,不看"这次打印对不对",看的是:

  • 代码能否被编译器证明安全
  • 并发边界是否清晰
  • 状态是否按职责被建模(DTO vs Stateful)
  • 团队是否有统一约束来避免隐患扩散

一句话收尾 :跨域传值用 Sendable struct,共享可变状态用 actor@unchecked Sendable 只做最后兜底。

相关推荐
大熊猫侯佩21 小时前
WWDC26 全新 SwiftData 的 .codable 宏选项:它不是泛型专用胶水
数据库·swift·apple
大龄秃头程序员1 天前
Swift 并发实战:从 Task 到 Actor 重入性,我掉进了“逻辑不一致”的坑
swift
LinXunFeng1 天前
给 Flutter 插件加上 Swift Package Manager 支持
flutter·swift
大龄秃头程序员1 天前
Swift 面试反直觉:Property Wrapper 真的拿不到宿主 self 吗?
swift
FeliksLv1 天前
从 +load 到 Swift Macros:自动注册的演进与实践
ios·objective-c·swift
语歌2 天前
AI 语言学习系统的工程边界:为什么 LLM 不该负责复习排期
人工智能·swift
2501_916008893 天前
iOS应用开发工具全面解析:如何选择与优化开发效率
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
大龄秃头程序员3 天前
Swift 属性包装器进阶:从“语法糖”到“并发安全”的 5 个深坑
swift