Swift 并发编程新选择:Mutex 保护可变状态实战解析

前言

Swift 5.5 带来 async/await 与 Actor 后,「用 Actor 包一层」几乎成了默认答案。

但在日常开发里,我们经常会遇到两种尴尬:

  1. 只想保护一个计数器、缓存或 token,却不得不把整段逻辑都改成异步;
  2. 把对象放到 @MainActor 后,发现后台线程也要用,结果到处是 await。

Apple 在 Swift 5.9 前后把 Mutex 正式搬进标准库(通过 Synchronization 模块),给"同步但不想异步"的场景提供了第三条路。

Mutex 是什么(一句话先记住)

Mutex = 互斥锁,同步、阻塞、轻量。

它只干一件事:同一时刻最多一个线程进入临界区,保证对共享状态的"读-改-写"原子化。

与 Actor 的"异步消息"不同,Mutex 的等待是阻塞线程,所以临界区必须短、快、不阻塞。

基础用法:从 0 到 1 保护一个计数器

  1. 引入模块(Xcode 15+/Swift 5.9 自带)
swift 复制代码
import Synchronization
  1. 定义线程安全的 Counter
swift 复制代码
final class Counter: Sendable {          // ① Sendable 空标记即可,Mutex 本身已 Sendable
    private let mutex = Mutex(0)         // ② 初始值 0

    /// 加 1,同步返回
    func increment() {
        mutex.withLock { value in
            value += 1                   // ③ 闭包内 value 是 inout,直接改
        }
    }

    /// 减 1
    func decrement() {
        mutex.withLock { value in
            value -= 1
        }
    }

    /// 读值,也要拿锁
    var count: Int {
        mutex.withLock { value in
            return value                 // ④ 只读,同样原子
        }
    }
}
  1. 客户端代码------完全同步
swift 复制代码
let counter = Counter()
counter.increment()
print(counter.count)   // 1

要点回顾

  • withLock<T> 泛型返回,既能读也能写;
  • 闭包里的 valueinout,修改即生效;
  • 锁的持有时间 = 闭包运行时间,务必短。

让属性看起来"像正常变量"------封装 getter/setter

swift 复制代码
extension Counter {
    var count: Int {
        get {
            mutex.withLock { $0 }        // $0 就是 value,直接返回
        }
        set {
            mutex.withLock { value in
                value = newValue
            }
        }
    }
}

// 使用方无感
counter.count = 10
print(counter.count) // 10

与 @Observable 搭档------让 SwiftUI 刷新

Mutex 只保护值,不会触发属性观察器。

若直接 @Observable final class Counter,视图不会刷新。

需要手动告诉 Observation 框架:

swift 复制代码
@Observable
final class Counter: Sendable {
    private let mutex = Mutex(0)

    var count: Int {
        get {
            access(keyPath: \.count)          // ① 读标记
            return mutex.withLock { $0 }
        }
        set {
            withMutation(keyPath: \.count) {  // ② 写标记
                mutex.withLock { $0 = newValue }
            }
        }
    }
}

SwiftUI 端无额外成本:

swift 复制代码
struct ContentView: View {
    @State private var counter = Counter()

    var body: some View {
        VStack {
            Text("\(counter.count)")
            Button("++") { counter.increment() }
            Button("--") { counter.decrement() }
        }
    }
}

Actor or Mutex?一张决策表帮你 10 秒选

维度 Mutex Actor
同步/异步 同步、阻塞 异步、非阻塞
适用场景 极短临界区(赋值、累加) 长时间任务、IO、网络
性能 极轻量,纳秒级锁 微秒毫秒,调度开销
语法侵入 无 async 强制 async/await
Sendable Mutex 已 Sendable,类标即可 Actor 引用即 Sendable
调试难度 简单,栈清晰 异步堆栈难追踪

"只想保护一两行, Mutex 别犹豫; 流程长、要并发, Actor 顶上。"

扩展场景实战

  1. 高频读写缓存(图片、Token)
swift 复制代码
final class ImageCache: Sendable {
    private let cache = Mutex([String: Image]())

    func image(for key: String) -> Image? {
        cache.withLock { $0[key] }
    }

    func save(_ image: Image, for key: String) {
        cache.withLock { dict in
            dict[key] = image
        }
    }
}
  1. 统计接口 QPS
swift 复制代码
final class Stats: Sendable {
    private let counter = Mutex(0)
    private let start = Date()

    func record() {
        counter.withLock { $0 += 1 }
    }

    var qps: Double {
        counter.withLock { Double($0) / start.timeIntervalSinceNow * -1 }
    }
}
  1. 保护非 Sendable 的 C 句柄
swift 复制代码
final class SQLiteHandle: @unchecked Sendable {
    private let db: UnsafeMutableRawPointer
    public init(db: UnsafeMutableRawPointer) {
        self.db = db
    }
    
    private let lock = Mutex(())

    func execute(_ sql: String) {
        lock.withLock { _ in
            sqlite3_exec(db, sql, nil, nil, nil)   // 临界区
        }
    }
}

踩坑与提醒

  1. 长任务别用 Mutex

    一旦临界区阻塞 IO,整个线程池都会被卡死,比 Actor 还惨。

  2. 递归加锁会死锁

    Mutex 不可重入,同一线程重复拿锁直接挂起;Actor 不会。

  3. 锁粒度要细

    大对象整颗锁会变成性能瓶颈,可拆成多颗 Mutex 或按 Key 分片。

  4. Swift 6 数据竞争检查

    打开 -strict-concurrency=complete 后,凡是非 Sendable 全局变量都会报错;用 Mutex 包一层即可通过。

小结

Actor 把"线程安全"装进黑盒子,让开发者用消息思考;Mutex 把"锁"暴露给你,却换回最简洁的同步代码。

两者不是谁取代谁,而是互补:

  • 短、频、快 → Mutex
  • 长、流、异步 → Actor
相关推荐
HarderCoder1 天前
Swift 模式:解构与匹配的安全之道
swift
东坡肘子1 天前
Swift 官方发布 Android SDK | 肘子的 Swift 周报 #0108
android·swiftui·swift
YGGP3 天前
【Swift】LeetCode 53. 最大子数组和
swift
2501_916008893 天前
用多工具组合把 iOS 混淆做成可复用的工程能力(iOS混淆|IPA加固|无源码混淆|Ipa Guard|Swift Shield)
android·开发语言·ios·小程序·uni-app·iphone·swift
胎粉仔3 天前
Swift 初阶 —— inout 参数 & 数据独占问题
开发语言·ios·swift·1024程序员节
HarderCoder3 天前
Swift 下标(Subscripts)详解:从基础到进阶的完整指南
swift
YGGP3 天前
【Swift】LeetCode 189. 轮转数组
swift
JZXStudio3 天前
5.A.swift 使用指南
框架·swift·app开发
非专业程序员Ping4 天前
HarfBuzz概览
android·ios·swift·font