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

相关推荐
2501_915909062 小时前
深入理解HTTPS和HTTP的区别、工作原理及安全重要性
安全·http·ios·小程序·https·uni-app·iphone
青衫码上行2 小时前
【Java Web学习 | 第九篇】JavaScript(3) 数组+函数
java·开发语言·前端·javascript·学习
jf加菲猫2 小时前
第1章 认识Qt
开发语言·c++·qt·ui
猪哥帅过吴彦祖2 小时前
Flutter 从入门到精通:状态管理入门 - setState 的局限性与 Provider 的优雅之道
android·flutter·ios
铅笔小新z3 小时前
深入理解C语言内存管理:从栈、堆到内存泄露与悬空指针
c语言·开发语言
m0_495562783 小时前
Swift-Enum
java·算法·swift
m0_495562783 小时前
Swift-snapKit使用
开发语言·elasticsearch·swift
狂团商城小师妹3 小时前
JAVA国际版同城服务同城信息同城任务发布平台APP源码Android + IOS
android·java·ios
q***18843 小时前
搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程
开发语言·后端·golang