Swift 初阶 —— Sendable 协议 & data races

一、data races

1、什么是 data races

官方文档对 data races 定义的解释是:

意思就是说 data races 就是多线程间没有同步地访问可变变量. 换句话说, data races 的定义就是: 在同时有读线程和写线程的情况下, 多线程间没有以串行的方式去访问同一块连续内存.

2、data races 是如何产生的

关于 data races 的产生原因, Swift 的官方文档是这样解释的:

总体意思就是说: 如果已经有一个线程在写这块内存, 但此时又有另一个线程访问这块内存内存, 那么就会产生 data races.

3、data races 例子

Swift 复制代码
import Foundation

final class Counter: @unchecked Sendable { // 为了消 warning 才用的 @unchecked Sendable, 但其实 value 是线程不安全的, 会造成 data races
    var value = 0
}

let counter = Counter()
let queue = DispatchQueue.global()

for _ in 0..<10_000 {
    queue.async {
        // 这里没有任何同步保护,多线程同时读写 value
        // 很容易出现 data race,导致最终值小于 10_000
        counter.value += 1
    }
}

// 简单等待一下所有任务(非常粗糙,只是为了示例)
Thread.sleep(forTimeInterval: 1.0)
print("最终结果:\(counter.value)")

结果小于 10000 的原因是这样的, 假如只有两个线程, 且 counter.value 的值为 9997:

|----|---------------------------------------------------|---------------------------------------------------|
| 步骤 | 线程 A | 线程 B |
| 1 | 读 counter.value, 寄存器a 值为 9997 | |
| 2 | | 读 counter.value, 寄存器b 值为 9997 |
| 3 | 寄存器a 执行 +1 操作, 此时寄存器a 值为 9998 | |
| 4 | | 寄存器b 执行 +1 操作, 此时寄存器a 值为 9998 |
| 5 | 把寄存器a 的值写进 counter.value, 此时 conuter.value 为 9998 | |
| 6 | | 把寄存器b 的值写进 counter.value, 此时 conuter.value 为 9998 |
[counter.value 并发执行过程]

于是这就导致了 counter.value 本应为 9999 的, 经过两次 +1 后, 变成了 9998.

二、 concurrency domain

1、什么是 concurrency domain

根据 Swift 的官方文档, 它是这样定义 concurrency domain 的:

中文翻译就是: 在一个 task 或 actor 的内部, 包含可变参数或可变属性的那部分代码就是一个 concurrency domain.

举个 🌰:

Swift 复制代码
@MainActor
final class ViewModel: ObservableObject {
    @Published var message: String = "准备中..."

    func loadData() {
        // 当前方法在 MainActor 隔离域内执行
        Task {
            // 切换到后台隔离域执行耗时工作
            let text = await fetchRemoteText()
            // 回到 MainActor 更新 UI
            self.message = text
        }
    }

    private func fetchRemoteText() async -> String {
        // detached task 将作业放到独立的 concurrency domain
        await Task.detached(priority: .background) {
            try? await Task.sleep(nanoseconds: 1_000_000_000) // 模拟网络延迟
            return "数据加载完成"
        }.value
    }
}

因为 ViewModel 是一个 Actor, 且 ViewModel 内有个可变的变量 (message) , 所以 ViewModel 就是一个 concurrency domain.

2、什么是 concurrency domain 间的传递

简单来说就是通过调 actor 函数或闭包的方式把某个可变变量或属性传进一个 actor 中执行, 又或是把某个变量或属性通过闭包捕获被某个 Task 或 actor 的闭包访问, 那么就属于跨 concurrency domain 传递.

举个🌰:

Swift 复制代码
// e.g. 普通 Task 捕获值(MainActor 域 → Task 域)
struct Score: Sendable {
    var value: Int
}

func startWork() {
    let score = Score(value: 42)      // 创建于 MainActor(主线程)
    Task.detached { [score] in
        // 这里进入 Task 的并发域,score 被安全传递进来
        print("当前分数:\(score.value)")
    }
}

// e.g. 在不同 actor 间传递
actor DataStore {
    private var items: [Int] = []

    func add(value: Int) {
        items.append(value)
    }

    func takeSnapshot() -> [Int] {   // [Int] 默认符合 Sendable
        items
    }
}

struct Report: Sendable {
    let values: [Int]
}

func fetchReport(store: DataStore) async -> Report {
    let snapshot = await store.takeSnapshot()
    // snapshot 从 DataStore 的 actor 域,传到调用者所在的并发域
    return Report(values: snapshot)
}

三、Sendable 协议

1、Sendable 协议用处

sendable 就是为了确保这个类的数据是线程安全的. 也就是说, 如果一个类遵循 sendable, 那么这个类只能被多个 task 以串行的方式访问.

2、如何遵循 Sendable 协议

2.1. 隐式遵循

符合以下情况的 Swift 会自动 (隐式) 认为它遵循 sendable:

  • 所有 Actor 自动遵循 sendable
  • 所有基本值类型 (Array, Int, Dict......) 都遵循 sendable
  • 所有存储属性 (包括闭包) 都遵循 sendable 的 Struct
  • 所有关联值类型都遵循 sendable 的 Enum
  • class.
    • 该 class 不是某个类 (除 NSObject) 的子类, 或 class 只继承了 NSObject
    • 该 class 的所有存储属性 (包括闭包) 都遵循 sendable
    • 该 class 为 final class

2.2. @unchecked Sendable & Mutex

2.2.1. @unchecked Sendable

因为 Sendable 的目的是确保同一块内存被多个 Task 串行访问, 而不是被并发访问; 因此我们可以在一个类里用 gcd 或锁实现内存的串行访问, 来达到 Sendable 的目的. 但 DispatchQueue 和 NSLock 都是引用类型, 且不遵循 Sendable, 因此 Swift 会认为这个类肯定不遵循 Sendable. 那在跨 concurrency domain 的时候如何解决呢? 其中一种方式就是用 @unchecked Sendable 告诉 Swift 这个类已经遵循 Sendable 了, 绕过 Swift 对这个类 Sendable 的检查.

举个🌰:

Swift 复制代码
final class ThreadSafeCache: @unchecked Sendable {
    private var cache: [String: Sendable] = [:]
    // 虽然有可变状态,但通过队列保证了线程安全
    private let queue = DispatchQueue(label: "cache", attributes: .concurrent)
    
    func get(_ key: String) -> Sendable? {
        queue.sync {
            cache[key]
        }
    }
    
    func set(_ key: String, value: Sendable) {
        queue.async(flags: .barrier) {
            self.cache[key] = value
        }
    }
}

但为了防止 @unchecked Sendable 被人滥用, Swift6 提出了 Synchronization 模块的 Mutex 类型. 这个类型可以实现真正的 Sendable, 无需再依赖 @uncheck.

2.2.2. Synchronization 模块的Mutex

Synchronization 模块的 Mutex 类型可以实现真正的 Sendable, 无需再依赖 @uncheck.

Swift 复制代码
import Synchronization

final class ThreadSafeCache: Sendable {
    private let cache = Mutex<[String: Sendable]>([:])

    func get(_ key: String) -> Sendable? {
        cache.withLock {
            $0[key]
        }
    }

    func set(_ key: String, value: Sendable) {
        cache.withLock {
            $0[key] = value
        }
    }
}

四、@sendable 声明闭包线程安全

1、@sendable 用处

@sendable 就是用来修饰闭包的, 来确保闭包捕获的变量都遵循 sendable 协议; 以此来确保被闭包捕获的变量都是线程安全的.

2、如何使用 @sendable

Swift 复制代码
func runLater(_ function: @escaping @Sendable () -> Void) -> Void {
    DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: function)
}

五、语法糖 ------ 防止 Swift 的隐式推理是否遵循协议

你可以通过下面两种方式来阻止 Swift 隐式推理是否遵循协议

Swift 复制代码
// ------------------------ 方式 1: @available(*, unavailable) 修饰 ---------------------------
@available(*, unavailable)
extension FileDescriptor: Sendable { }

// ------------------------ 方式 2: 协议名前加 '~' ---------------------------
struct FileDescriptor: ~Sendable {
    let rawValue: Int
}

本文参考:
Swift sendable typeshttps://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Sendable-TypesiOS data raceshttps://developer.apple.com/documentation/xcode/data-races

Swift6 sendable typeshttps://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/dataracesafety#Sendable-Types

hacking with swifthttps://www.hackingwithswift.com/swift/5.5/sendablehttps://www.avanderlee.comhttps://www.avanderlee.com/swift/sendable-protocol-closures/

By 东坡肘子:https://fatbobman.com/zh/posts/sendable-sending-nonsending/#unchecked-sendable

相关推荐
周杰伦fans6 分钟前
构建真正智能的CAD AI绘图代理
开发语言·人工智能·c#
zhanghaha13142 小时前
Python进阶教程:35_PyQt (保姆级零基础教程)
开发语言·python·pyqt
释厄6233 小时前
多黑洞具现论·物理/生物/金融三域显化·虚拟宇宙黑洞·奇点=0 圆圈
开发语言·数据结构·人工智能·程序人生·安全威胁分析
Fruit_Caller4 小时前
static
开发语言·c++
茶底世界之下9 小时前
导出进度不是一个百分比:AVFoundation 收尾阶段的状态建模
ios·swift
大熊猫侯佩9 小时前
macOS27 上可变界面尺寸 iOS App 揭秘
macos·ios·swiftui
用户2181697049309 小时前
LLVM
ios
茶底世界之下9 小时前
为什么媒体源不能在回调仍执行时提前报告 finish?
swift
zeqinjie9 小时前
Flutter 获取 iPhone Duo 预留区位置
前端·flutter·ios
codigger9 小时前
ObjectSense:一门千行内核、把可靠性写进骨子里的面向对象脚本语言
开发语言·编程·编程语言