WWDC Quick Look 💓 By SwiftGGTeam
用 Swift Actor 保护可变状态

用 Swift Actor 保护可变状态

观看原视频

用 Swift Actor 保护可变状态

Highlight

Actor 将类的可变状态自动隔离在串行执行域中,外部访问必须通过 await。编译器会强制检查这条规则——忘记 await 代码直接编译失败。一个原本需要手动加锁的计数器,用 actor 只需把 class 改成 actor,线程安全由语言和运行时保证。

核心内容

数据竞争(Data Race)是并发编程中最隐蔽的 bug 之一:两个线程同时读写同一块可变内存,结果不可预测,复现困难,调试更是噩梦。传统解决方案是用锁(NSLock)或串行派发队列(DispatchQueue)保护共享状态,但容易遗漏、难以测试、还有死锁风险。

Swift 5.5 引入的 actor 类型在语言层面解决了这个问题。Actor 是一种特殊的引用类型,它的所有可变状态都被隔离在 actor 内部。外部代码访问 actor 的属性或方法时,必须通过 await 显式标记这是一个潜在的挂起点。编译器强制检查这条规则,将数据竞争从运行时错误转变为编译时错误。

详细内容

数据竞争问题

一个简单的计数器,用 class 实现时存在数据竞争(00:42):

class Counter {
    var value = 0
    func increment() -> Int {
        value = value + 1
        return value
    }
}

let counter = Counter()

Task.detached {
    print(counter.increment()) // data race!
}
Task.detached {
    print(counter.increment()) // data race!
}

两个 Task 同时调用 incrementvalue = value + 1 不是原子操作,结果不可预测。

值类型的局限

值类型(struct)天然没有数据竞争,因为每次赋值都是拷贝(02:20):

var array1 = [1, 2]
var array2 = array1
array1.append(3)
array2.append(4)
print(array1) // [1, 2, 3]
print(array2) // [1, 2, 4]

但值类型无法表达共享可变状态。把 Counter 改成 struct(02:59),每个 Task 拿到的是独立拷贝,结果永远是 1:

struct Counter {
    var value = 0
    mutating func increment() -> Int {
        value = value + 1
        return value
    }
}

Task.detached {
    var counter = counter
    print(counter.increment()) // always prints 1
}

Actor 隔离

class 改为 actor(05:23),编译器自动保证串行访问:

actor Counter {
    var value = 0
    func increment() -> Int {
        value = value + 1
        return value
    }
}

let counter = Counter()

Task.detached {
    print(await counter.increment())
}
Task.detached {
    print(await counter.increment())
}

Actor 内部的方法调用是同步的(07:51)。resetSlowly 方法内部多次调用 increment,不需要 await

extension Counter {
    func resetSlowly(to newValue: Int) {
        value = 0
        for _ in 0..<newValue {
            increment()  // 同步调用,无需 await
        }
        assert(value == newValue)
    }
}

挂起后的状态假设

await 会挂起当前执行,期间 actor 状态可能被其他任务修改。ImageDownloader 的缓存逻辑有一个经典 bug(09:02):

actor ImageDownloader {
    private var cache: [URL: Image] = [:]

    func image(from url: URL) async throws -> Image? {
        if let cached = cache[url] {
            return cached
        }
        let image = try await downloadImage(from: url)
        // Bug: await 期间 cache 可能已被其他任务修改
        cache[url] = image
        return image
    }
}

await downloadImage 期间,另一个任务可能已经完成同一 URL 的下载并写入了缓存。解决方案(11:59):用 inProgress 状态防止重复下载:

actor ImageDownloader {
    private enum CacheEntry {
        case inProgress(Task<Image, Error>)
        case ready(Image)
    }
    private var cache: [URL: CacheEntry] = [:]

    func image(from url: URL) async throws -> Image? {
        if let cached = cache[url] {
            switch cached {
            case .ready(let image):
                return image
            case .inProgress(let task):
                return try await task.value  // 等待已有任务完成
            }
        }

        let task = Task { try await downloadImage(from: url) }
        cache[url] = .inProgress(task)

        do {
            let image = try await task.value
            cache[url] = .ready(image)
            return image
        } catch {
            cache[url] = nil
            throw error
        }
    }
}

协议遵循与 Actor 隔离

静态方法天然在 actor 外部执行(13:30):

actor LibraryAccount {
    let idNumber: Int
    var booksOnLoan: [Book] = []
}

extension LibraryAccount: Equatable {
    static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
        lhs.idNumber == rhs.idNumber  // 静态方法,无需 await
    }
}

实例方法可以用 nonisolated 标记为在 actor 外部执行(14:15),但只能访问 let 常量:

extension LibraryAccount: Hashable {
    nonisolated func hash(into hasher: inout Hasher) {
        hasher.combine(idNumber)  // idNumber 是 let,安全
    }
}

跨 Actor 传递数据

Actor 之间传递数据时,值类型(struct)是安全的,因为传递的是拷贝(17:15):

struct Book {
    var title: String
    var authors: [Author]
}

func visit(_ account: LibraryAccount) async {
    guard var book = await account.selectRandomBook() else { return }
    book.title = "\(book.title)!!!"  // OK: 修改的是本地拷贝
}

引用类型(class)则存在风险(17:39)。从 actor 取出的 class 实例,外部修改会影响 actor 内部状态:

class Book {
    var title: String
}

func visit(_ account: LibraryAccount) async {
    guard var book = await account.selectRandomBook() else { return }
    book.title = "\(book.title)!!!"  // Not OK: 修改的是共享引用
}

Sendable 协议

Sendable 协议标记可以安全跨并发域传递的类型(20:08)。struct 自动满足 Sendable(如果所有属性都满足):

struct Book: Sendable {
    var title: String
    var authors: [Author]
}

泛型类型可以用条件遵循(20:43):

struct Pair<T, U> {
    var first: T
    var second: U
}

extension Pair: Sendable where T: Sendable, U: Sendable { }

Main Actor

UIKit 要求所有 UI 操作在主线程执行。传统做法是用 DispatchQueue.main.async(24:19):

func checkedOut(_ booksOnLoan: [Book]) {
    booksView.checkedOutBooks = booksOnLoan
}
DispatchQueue.main.async {
    checkedOut(booksOnLoan)
}

@MainActor 让编译器保证函数始终在主线执行(25:01):

@MainActor func checkedOut(_ booksOnLoan: [Book]) {
    booksView.checkedOutBooks = booksOnLoan
}

await checkedOut(booksOnLoan)  // Swift 确保在主线程执行

类也可以标记为 @MainActor(26:21),其所有方法隐式在主线程执行:

@MainActor class MyViewController: UIViewController {
    func onPress(...) { ... }  // 隐式 @MainActor

    nonisolated func fetchLatestAndDisplay() async { ... }  // 显式脱离主线程
}

核心启发

  1. 用 actor 替代手动锁:新项目直接用 actor 替代 NSLock 和串行 DispatchQueue。老项目迁移时,先把最外层的状态管理对象改为 actor。

  2. 警惕 await 后的状态变化:在 actor 方法中,await 挂起后重新执行时,actor 状态可能已被修改。不要假设 await 前的判断仍然成立。

  3. inProgress 模式防止重复工作:对于耗时操作(如网络下载),用枚举状态标记”进行中”,让后续请求等待同一任务完成,而不是发起重复请求。

  4. 优先使用值类型跨 actor 传递数据:struct 的拷贝语义天然安全。如果必须用 class,确保其实现 Sendable 或不会在 actor 外部被修改。

  5. @MainActor 替代 DispatchQueue.main.async@MainActor 在编译期保证线程安全,比运行时的派发队列更可靠。ViewController、ViewModel 等与 UI 相关的类型可以直接标记为 @MainActor

关联 Session

评论

GitHub Issues · utterances