Sendable 和 @Sendable 閉包 —— 代碼實例詳解
Sendable
和 @Sendable
是 Swift 5.5 中的并發(fā)修改的一部分弓叛,解決了結構化的并發(fā)結構體和執(zhí)行者消息之間傳遞的類型檢查的挑戰(zhàn)性問題沪摄。
在深入探討Sendable
的話題之前侈沪,我鼓勵你閱讀我圍繞 async/await摩桶、actors 和 actor isolation 的文章。這些文章涵蓋了新的并發(fā)性變化的基礎知識,它們與本文所解釋的技術直接相關烦租。
我應該在什么時候使用 Sendable丸冕?
Sendable
協議和閉包表明那些傳遞的值的公共API是否線程安全的向編譯器傳遞了值耽梅。當沒有公共修改器、有內部鎖定系統(tǒng)或修改器實現了與值類型一樣的復制寫入時胖烛,公共API可以安全地跨并發(fā)域使用眼姐。
標準庫中的許多類型已經支持了Sendable
協議,消除了對許多類型添加一致性的要求佩番。由于標準庫的支持众旗,編譯器可以為你的自定義類型創(chuàng)建隱式一致性。
例如答捕,整型支持該協議:
extension Int: Sendable {}
一旦我們創(chuàng)建了一個具有Int
類型的單一屬性的值類型結構體逝钥,我們就隱式地得到了對Sendable
協議的支持。
// 隱式地遵守了 Sendable 協議
struct Article {
var views: Int
}
與此同時拱镐,同樣的Article
內容的類艘款,將不會有隱式遵守該協議:
// 不會隱式的遵守 Sendable 協議
class Article {
var views: Int
}
類不符合要求,因為它是一個引用類型沃琅,因此可以從其他并發(fā)域變異哗咆。換句話說,該類文章(Article
)的傳遞不是線程安全的益眉,所以編譯器不能隱式地將其標記為遵守Sendable
協議晌柬。
使用泛型和枚舉時的隱式一致性
很好理解的是,如果泛型不符合Sendable
協議郭脂,編譯器就不會為泛型添加隱式的一致性年碘。
// 因為 Value 沒有遵守 Sendable 協議,所以 Container 也不會自動的隱式遵守該協議
struct Container<Value> {
var child: Value
}
然而展鸡,如果我們將協議要求添加到我們的泛型中屿衅,我們將得到隱式支持:
// Container 隱式地符合 Sendable,因為它的所有公共屬性也是如此莹弊。
struct Container<Value: Sendable> {
var child: Value
}
對于有關聯值的枚舉也是如此:
你可以看到,我們自動從編譯器中得到一個錯誤:
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)
}
從線程安全的實例中拋出錯誤
同樣的規(guī)則適用于想要符合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
協議的一致性必須發(fā)生在同一個源文件中,以確保編譯器檢查所有可見成員的線程安全中狂。
例如凫碌,你可以在例如 Swift package這樣的模塊中定義以下類型:
public struct Article {
internal var title: String
}
Article
是公開的,而標題title
是內部的胃榕,在模塊外不可見盛险。因此,編譯器不能在源文件之外應用Sendable
一致性勋又,因為它對標題屬性不可見苦掘,即使標題使用的是遵守Sendable
協議的String
類型。
同樣的問題發(fā)生在我們想要使一個可變的非最終類遵守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
函數可以跨并發(fā)域傳遞,因此也需要可發(fā)送的一致性倍试。然而讯屈,函數不能符合協議,所以Swift引入了@Sendable
屬性县习。你可以傳遞的函數的例子是全局函數聲明涮母、閉包和訪問器,如getters
和setters
躁愿。
SE-302的部分動機是執(zhí)行盡可能少的同步
我們希望這樣一個系統(tǒng)中的絕大多數代碼都是無同步的叛本。
使用@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
來快速解決這種情況梆砸,但它展示了編譯器如何幫助我們執(zhí)行線程安全转质。
Swift 6: 為你的代碼啟用嚴格的并發(fā)性檢查
Xcode 14 允許您通過 SWIFT_STRICT_CONCURRENCY
構建設置啟用嚴格的并發(fā)性檢查。
這個構建設置控制編譯器對Sendable
和actor-isolation
檢查的執(zhí)行水平:
-
Minimal : 編譯器將只診斷明確標有
Sendable
一致性的實例休蟹,并等同于Swift 5.5和5.6的行為。不會有任何警告或錯誤日矫。 -
Targeted: 強制執(zhí)行
Sendable
約束赂弓,并對你所有采用async/await
等并發(fā)的代碼進行actor-isolation
檢查。編譯器還將檢查明確采用Sendable
的實例搬男。這種模式試圖在與現有代碼的兼容性和捕捉潛在的數據競賽之間取得平衡拣展。 - Complete: 匹配預期的 Swift 6語義,以檢查和消除數據競賽缔逛。這種模式檢查其他兩種模式所做的一切备埃,并對你項目中的所有代碼進行這些檢查。
嚴格的并發(fā)檢查構建設置有助于 Swift 向數據競賽安全邁進褐奴。與此構建設置相關的每一個觸發(fā)的警告都可能表明你的代碼中存在潛在的數據競賽按脚。因此,必須考慮啟用嚴格并發(fā)檢查來驗證你的代碼敦冬。
Enabling strict concurrency in Xcode 14
你會得到的警告數量取決于你在項目中使用并發(fā)的頻率辅搬。對于Stock Analyzer,我有大約17個警告需要解決:
這些警告可能讓人望而生畏堪遂,但利用本文的知識介蛉,你應該能夠擺脫大部分警告,防止數據競賽的發(fā)生溶褪。然而币旧,有些警告是你無法控制的,因為是外部模塊觸發(fā)了它們猿妈。在我的例子中吹菱,我有一個與SWHighlight
有關的警告彭则,它不符合Sendable
输瓜,而蘋果在他們的SharedWithYou
框架中定義了它担忧。
在上述SharedWithYou
框架的例子中最欠,最好是等待庫的所有者添加Sendable
支持。在這種情況下拌阴,這就意味著要等待蘋果公司為SWHighlight
實例指明Sendable
的一致性厂镇。對于這些庫,你可以通過使用@preconcurrency
屬性來暫時禁用Sendable
警告:
@preconcurrency import SharedWithYou
重要的是要明白喇辽,我們并沒有解決這些警告舅世,而只是禁用了它們摩钙。來自這些庫的代碼仍然有可能發(fā)生數據競賽。如果你正在使用這些框架的實例,你需要考慮實例是否真的是線程安全的辟汰。一旦你使用的框架被更新為Sendable
的一致性凑术,你可以刪除@preconcurrency
屬性壮莹,并修復可能觸發(fā)的警告。
繼續(xù)您的 Swift 并發(fā)之旅
并發(fā)更改不僅僅是 async-await命满,還包括許多您可以在代碼中受益的新功能涝滴。所以當你在做的時候,為什么不深入研究其他并發(fā)特性呢?
- Swift 中的 async/await
- Swift 中的 async let
- Swift 中的 Task
- Swift 中的 Actors 使用以如何及防止數據競爭
- Swift 中的 MainActor 使用和主線程調度
- 理解 Swift Actor 隔離關鍵字:nonisolated 和 isolated
- Swift 中的 Sendable 和 @Sendable 閉包
- Swift 中的 AsyncThrowingStream 和 AsyncStream
- Swift 中的 AsyncSequence
轉自 Sendable and @Sendable closures explained with code examples