Swift 并发:Actor、isolated、nonisolated 完全导读

Actor 是什么?

Actor 是引用类型(与 class 类似),但自带隔离域:

  • 任意时刻只允许一个任务进入 Actor 内部
  • 外部调用必须加 await,自动排队
  • 编译器保证无数据竞争,无需手动加锁

对比:手动锁 vs Actor

swift 复制代码
// ❌ 旧世界:DispatchQueue + 锁
class Counter {
    private var value = 0
    private let queue = DispatchQueue(label: "counter")
    
    func increment() {
        queue.sync { value += 1 }
    }
}

// ✅ 新世界:Actor
actor Counter {
    var value = 0
    
    func increment() {
        value += 1          // 无需锁,编译器保护
    }
}

→ 代码量减半,永远不会忘记加锁。

Actor 使用范式

swift 复制代码
let counter = Counter()

Task {
    await counter.increment()           // 必须 await
    let current = await counter.value   // 串行排队
    print("Value \(current)")
}

规则速记:"进 Actor 先 await,出来无需 await。"

nonisolated: actor 里的"快速通道"

当方法不触碰 Actor 的任何可变状态时,可标 nonisolated

  • 跳过排队 → 性能更高
  • 无需 await → 调用方更轻松
  • 但不能读/写任何属性(除非属性也是 nonisolated

示例

swift 复制代码
actor Logger {
    nonisolated func appName() -> String {
        "MyCoolApp"   // 只返回常量,无状态访问
    }
    
    func log(_ msg: String) {
        print("[\(Date())] \(msg)")  // 访问状态,需 await
    }
}

Task {
    print(Logger().appName())      // ✅ 同步调用
    await Logger().log("Hello")    // ✅ 需 await
}

isolated:把"外部函数"拉进 actor 内部

场景:在 extension 或工具方法里,想同步访问 actor 状态,又不想 await

做法:把 actor 自身作为参数标 isolated

swift 复制代码
actor StatsTracker {
    var total = 0
    var count = 0
    
    func showAverage() {
        // 身处 actor 内部,可直接调用
        print(average(from: self))
    }
}

extension StatsTracker {
    // 外部函数,但获得"内部通行证"
    func average(from tracker: isolated StatsTracker) -> Double {
        Double(tracker.total) / Double(tracker.count)   // ✅ 无 await
    }
}

func average1(from tracker: isolated StatsTracker) -> Double {
    Double(tracker.total) / Double(tracker.count)   // ✅ 无 await
}

Task {
    let st = StatsTracker()
    // 这里调用的时候需要加await
    let s = await average1(from: st)
}

调用限制:

  • 只能在同一 actor 隔离域里使用
  • 外部世界无法直接 await average() → 编译器挡住

实战:UserService 完整例子

swift 复制代码
actor UserService {
    private var cache: [Int: String] = [:]

    // Actor 隔离:可能挂起
    func fetchUser(id: Int) async -> String {
        if let name = cache[id] { return name }
        
        try? await Task.sleep(nanoseconds: 500_000_000) // 模拟网络
        let name = "User\(id)"
        cache[id] = name
        return name
    }
    
    // 非隔离:立即返回
    nonisolated func version() -> String {
        "UserService v1.0"
    }
}

使用侧:

swift 复制代码
let service = UserService()

Task {
    print(service.version())              // 同步,无 await
    print(await service.fetchUser(id: 42)) // 第一次,慢
    print(await service.fetchUser(id: 42)) // 第二次,缓存瞬间返回
}

三条口诀一张图

场景 关键词 是否 await 性能
进 actor 内部 await 正常排队
不碰状态的工具 nonisolated 快速通道
外部函数想进内部 isolated Actor ❌(内部用) 零开销

常见编译错误速查

错误 原因 修复
Actor-isolated property can only be referenced with 'await' 少写 await 加上 await
Call to nonisolated function must not be marked with 'await' 多了 await 去掉 await
'isolated' parameter can only be called from within the same actor 外部直接调 isolated方法 把逻辑移到 actor 内部或使用普通扩展

什么时候用 Actor: checklist

✅ 适合

  • 多任务同时读/写同一可变状态(计数器、缓存、状态机)
  • 需要编译期保证无数据竞争
  • 与 SwiftUI、CoreData 主线程栈搭配

❌ 不适合

  • 只读常量或纯函数 → 用 struct
  • 跨隔离频繁大量小调用 → 队列可能成为瓶颈
  • 已有无锁队列/原子方案且极致性能

一句话总结

"Actor = 自动锁 + 编译器检查;nonisolated 开快道,isolated 让外部函数进内部。"

把 Actor 当作"线程安全保险箱":

重要物品(可变状态)放进去,拿的时候await排队;

只读文件(nonisolated)走侧门,不排队;

临时工具(isolated)让内部员工用,零等待。

用好这三板斧,写并发代码就像写单线程一样轻松!

相关推荐
_瑞1 天前
深入理解灵动岛
前端·ios·app
黑科技iOS上架1 天前
分享一个开源的iOS应用AppIcon生成器
经验分享·ios
wabil2 天前
【LVGL】滑动切换页面的界面优化实践
开发语言·ios·swift
威武的花瓣2 天前
调用Page.RegisterAsyncTask()的异步页
ios·iphone
Digitally3 天前
5 种简易方法:摩托罗拉手机数据迁移至 iPhone 17
ios·智能手机·iphone
EricStone5 天前
VibeCoding工程流程学习二:iOS项目架构
ios·vibecoding
天桥吴彦祖7 天前
判断iOS如何监听手机屏幕是否锁屏
ios
东坡肘子7 天前
SPI 加入 Apple,Swift 迈向自举 -- 肘子的 Swift 周报 #142
人工智能·swiftui·swift
敲代码的鱼8 天前
PDF 预览与签名批注写回 支持安卓 iOS 鸿蒙 UTS插件
android·前端·ios
时光足迹8 天前
uni-app 视频通话实战:康复师与患者视频问诊的 6 个致命 Bug 与解决方案
android·ios·uni-app