继续创作,加速生长!这是我参加「日新计划 10 月更文挑战」的第14天,点击查看活动详情

前言

Sendable@Sendable 是 Swift 5.5 中的并发修改的一部分,处理了结构化的并发结构体和履行者音讯之间传递的类型查看的挑战性问题。

运用 Sendable

应该在什么时候运用 Sendable

Sendable协议和闭包标明那些传递的值的公共API是否线程安全的向编译器传递了值。当没有公共修改器、有内部锁定系统或修改器实现了与值类型一样的仿制写入时,公共API能够安全地跨并发域运用。

标准库中的许多类型现已支撑了Sendable协议,消除了对许多类型增加一致性的要求。由于标准库的支撑,编译器能够为你的自定义类型创立隐式一致性。

例如,整型支撑该协议:

extension Int: Sendable {}

一旦咱们创立了一个具有 Int 类型的单一特点的值类型结构体,咱们就隐式地得到了对 Sendable 协议的支撑。

// 隐式地恪守了 Sendable 协议
struct Article {
    var views: Int
}

与此同时,相同的 Article 内容的类,将不会有隐式恪守该协议:

// 不会隐式的恪守 Sendable 协议
class Article {
    var views: Int
}

类不契合要求,由于它是一个引证类型,因而能够从其他并发域变异。换句话说,该类文章(Article)的传递不是线程安全的,所以编译器不能隐式地将其符号为恪守Sendable协议。

运用泛型和枚举时的隐式一致性

很好理解的是,假如泛型不契合Sendable协议,编译器就不会为泛型增加隐式的一致性。

// 由于 Value 没有恪守 Sendable 协议,所以 Container 也不会主动的隐式恪守该协议
struct Container<Value> {
    var child: Value
}

可是,假如咱们将协议要求增加到咱们的泛型中,咱们将得到隐式支撑:

// Container 隐式地契合 Sendable,由于它的一切公共特点也是如此。
struct Container<Value: Sendable> {
    var child: Value
}

关于有关联值的枚举也是如此:

Sendable 和 @Sendable 闭包 —— 代码实例详解

你能够看到,咱们主动从编译器中得到一个过错:

Associated value ‘loggedIn(name:)’ of ‘Sendable’-conforming enum ‘State’ has non-sendable type ‘(name: NSAttributedString)’

咱们能够经过运用一个值类型String来处理这个过错,由于它现已契合Sendable

enum State: Sendable {
    case loggedOut
    case loggedIn(name: String)
}

从线程安全的实例中抛出过错

相同的规则适用于想要契合Sendable的过错类型。

struct ArticleSavingError: Error {
    var author: NonFinalAuthor
}
extension ArticleSavingError: Sendable { }

由于作者不是不变的(non-final),并且不是线程安全的(后面会具体介绍),咱们会遇到以下过错:

Stored property ‘author’ of ‘Sendable’-conforming struct ‘ArticleSavingError’ has non-sendable type ‘NonFinalAuthor’

你能够经过保证ArticleSavingError的一切成员都契合Sendable协议来处理这个过错。

怎么运用Sendable协议

隐式一致性消除了许多咱们需求自己为Sendable协议增加一致性的状况。可是,在有些状况下,咱们知道咱们的类型是线程安全的,可是编译器并没有为咱们增加隐式一致性。

常见的比如是被符号为不行变和内部具有锁定机制的类:

/// User 是不行改变的,因而是线程安全的,所以能够恪守 Sendable 协议
final class User: Sendable {
    let name: String
    init(name: String) { self.name = name }
}

你需求用@unchecked特点来符号可变类,以标明咱们的类由于内部锁定机制所以是线程安全的:

extension DispatchQueue {
    static let userMutatingLock = DispatchQueue(label: "person.lock.queue")
}
final class MutableUser: @unchecked Sendable {
    private var name: String = ""
    func updateName(_ name: String) {
        DispatchQueue.userMutatingLock.sync {
            self.name = name
        }
    }
}

要在同一源文件中恪守 Sendable的束缚

Sendable协议的一致性有必要产生在同一个源文件中,以保证编译器查看一切可见成员的线程安全。

例如,你能够在例如 Swift package这样的模块中定义以下类型:

public struct Article {
    internal var title: String
}

Article 是公开的,而标题title是内部的,在模块外不行见。因而,编译器不能在源文件之外应用Sendable一致性,由于它对标题特点不行见,即使标题运用的是恪守Sendable协议的String类型。

相同的问题产生在咱们想要使一个可变的非终究类恪守Sendable协议时:

Sendable 和 @Sendable 闭包 —— 代码实例详解

由于该类对错终究的,咱们无法契合Sendable协议的要求,由于咱们不确定其他类是否会承继User的非Sendable成员。因而,咱们会遇到以下过错:

Non-final class ‘User’ cannot conform to Sendable; use @unchecked Sendable

正如你所看到的,编译器建议运用@unchecked Sendable。咱们能够把这个特点增加到咱们的User类中,并脱节这个过错:

class User: @unchecked Sendable {
    let name: String
    init(name: String) { self.name = name }
}

可是,这确实要求咱们无论何时从User承继,都要保证它是线程安全的。由于咱们给自己和同事增加了额定的责任,我不鼓励运用这个特点,建议运用组合、终究类或值类型来实现咱们的目的。

怎么运用 @Sendabele

函数能够跨并发域传递,因而也需求可发送的一致性。可是,函数不能契合协议,所以Swift引入了@Sendable特点。你能够传递的函数的比如是全局函数声明、闭包和拜访器,如getterssetters

SE-302的部分动机是履行尽或许少的同步

咱们希望这样一个系统中的绝大多数代码都是无同步的。

运用@Sendable特点,咱们将告诉编译器,他不需求额定的同步,由于闭包中一切捕获的值都是线程安全的。一个典型的比如是在Actor isolation中运用闭包。

actor ArticlesList {
    func filteredArticles(_ isIncluded: @Sendable (Article) -> Bool) async -> [Article] {
        // ...
    }
}

假如你用非 Sendabel 类型的闭包,咱们会遇到一个过错:

let listOfArticles = ArticlesList()
var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword")
let filteredArticles = await listOfArticles.filteredArticles { article in
    // Error: Reference to captured var 'searchKeyword' in concurrently-executing code
    guard let searchKeyword = searchKeyword else { return false }
    return article.title == searchKeyword.string
}

当然,咱们能够经过运用一个普通的String来快速处理这种状况,但它展示了编译器怎么协助咱们履行线程安全。

Swift 6: 为你的代码启用严厉的并发性查看

Xcode 14 允许您经过 SWIFT_STRICT_CONCURRENCY 构建设置启用严厉的并发性查看。

Sendable 和 @Sendable 闭包 —— 代码实例详解

这个构建设置操控编译器对Sendableactor-isolation查看的履行水平:

  • Minimal : 编译器将只诊断清晰标有Sendable一致性的实例,并等同于Swift 5.5和5.6的行为。不会有任何正告或过错。
  • Targeted: 强制履行Sendable束缚,并对你一切选用async/await等并发的代码进行actor-isolation查看。编译器还将查看清晰选用Sendable的实例。这种形式企图在与现有代码的兼容性和捕捉潜在的数据比赛之间取得平衡。
  • Complete: 匹配预期的 Swift 6语义,以查看和消除数据比赛。这种形式查看其他两种形式所做的一切,并对你项目中的一切代码进行这些查看。

严厉的并发查看构建设置有助于 Swift 向数据比赛安全跨进。与此构建设置相关的每一个触发的正告都或许标明你的代码中存在潜在的数据比赛。因而,有必要考虑启用严厉并发查看来验证你的代码。

Enabling strict concurrency in Xcode 14

你会得到的正告数量取决于你在项目中运用并发的频率。关于Stock Analyzer,我有大约17个正告需求处理:

Sendable 和 @Sendable 闭包 —— 代码实例详解

这些正告或许让人望而生畏,但利用本文的常识,你应该能够脱节大部分正告,避免数据比赛的产生。可是,有些正告是你无法操控的,由于是外部模块触发了它们。在我的比如中,我有一个与SWHighlight有关的正告,它不契合Sendable,而苹果在他们的SharedWithYou结构中定义了它。

在上述SharedWithYou结构的比如中,最好是等待库的一切者增加Sendable支撑。在这种状况下,这就意味着要等待苹果公司SWHighlight实例指明Sendable的一致性。关于这些库,你能够经过运用@preconcurrency特点来暂时禁用Sendable正告:

@preconcurrency import SharedWithYou

重要的是要理解,咱们并没有处理这些正告,而仅仅禁用了它们。来自这些库的代码仍然有或许产生数据比赛。假如你正在运用这些结构的实例,你需求考虑实例是否真的是线程安全的。一旦你运用的结构被更新为Sendable的一致性,你能够删除@preconcurrency特点,并修正或许触发的正告。

关于咱们

咱们是由 Swift 爱好者一起保护,咱们会分享以 Swift 实战、SwiftUI、Swift 根底为中心的技术内容,也整理收集优秀的学习材料。

后续还会翻译很多材料到咱们公众号,有感爱好的朋友,能够参加咱们。