Swift 中的 Sendable 和 @Sendable 閉包

Sendable 和 @Sendable 閉包 —— 代碼實例詳解

Sendable@Sendable 是 Swift 5.5 中的并發修改的一部分,解決了結構化的并發結構體和執行者消息之間傳遞的類型檢查的挑戰性問題。

在深入探討Sendable的話題之前,我鼓勵你閱讀我圍繞 async/awaitactorsactor isolation 的文章。這些文章涵蓋了新的并發性變化的基礎知識,它們與本文所解釋的技術直接相關。

我應該在什么時候使用 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協議的要求,因為我們不確定其他類是否會繼承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 的符合性

這個構建設置控制編譯器對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個警告需要解決:

并發相關的警告,表明潛在的數據競賽.

這些警告可能讓人望而生畏,但利用本文的知識,你應該能夠擺脫大部分警告,防止數據競賽的發生。然而,有些警告是你無法控制的,因為是外部模塊觸發了它們。在我的例子中,我有一個與SWHighlight有關的警告,它不符合Sendable,而蘋果在他們的SharedWithYou框架中定義了它。

在上述SharedWithYou框架的例子中,最好是等待庫的所有者添加Sendable支持。在這種情況下,這就意味著要等待蘋果公司為SWHighlight實例指明Sendable的一致性。對于這些庫,你可以通過使用@preconcurrency屬性來暫時禁用Sendable警告:

@preconcurrency import SharedWithYou

重要的是要明白,我們并沒有解決這些警告,而只是禁用了它們。來自這些庫的代碼仍然有可能發生數據競賽。如果你正在使用這些框架的實例,你需要考慮實例是否真的是線程安全的。一旦你使用的框架被更新為Sendable的一致性,你可以刪除@preconcurrency屬性,并修復可能觸發的警告。

繼續您的 Swift 并發之旅

并發更改不僅僅是 async-await,還包括許多您可以在代碼中受益的新功能。所以當你在做的時候,為什么不深入研究其他并發特性呢?

轉自 Sendable and @Sendable closures explained with code examples

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容