Sendable
#Concurrency #@Sendable
Sendable の必要性
Swift における Concurrency の最大の目標は、Data race を発生させることなく共有した状態を扱える機構を提供することであり、この機構の提供のために Actor が導入された。Actor の内部状態は他から独立しており (actor-isolated)、Actor 内部からのみ直接アクセスできる。しかし、Actor の保持する状態の種類によっては、Actor isolation が崩れてしまう可能性がある。
下記のような定義を考える。Person はクラス、すなわち参照型である。Account は actor である。Account の randomFriend() で内部状態である Person を返すようにすると、Person が参照型であるために、Actor 外部で状態が変更できてしまう、と言う問題が生じる。これは Data race を引き起こす可能性がある。
code:swift
class Person {
var name: String
}
actor Account {
var friends: Person
func randomFriend() -> Person?
}
let account = Account()
if let friend = await account.randomFriend() {
friend.name = "hoge" // actor 外部で 状態が変更できてしまう!!
}
Sendable/@Sendable
Sendable 及び @Sendable attribute は、それらに適合した値は、Actor 内外の境界で安全にやり取り (cross-actor reference) できることを表す。これに適合していない値をやりとりしようとした場合、コンパイル時エラーとなる。
Sendable
概要
Sendable は Marker Protocol として、以下のように定義されている。
code:swift
@_marker
protocol Sendable {}
ある型について、その public な API が並列に実行されても安全である (≒ Data race を生じさせない) ことが保証されているのであれば、Sendable に適合することを検討できる。値型はもちろん、内部的に Lock による同期機構を持った class や、mutator を持たない immutable な class なども、Sendable への適合を検討して良い。
ただし、安全であることが保証されていないにも関わらず Sendable に適合させた場合には Data race を生じさせる恐れがあるので注意する。
Conformance checking
ある型 Sendable に適合させようとすると、コンパイラにより conformance checking が行われる。struct や class の場合、保持する property は全て Sendable に適合していることが求められる。適合していない場合にはコンパイルエラーとなる。
code:swift
// OK
struct Person: Sendable {
var name: String
}
// OK
final class Person: Sendable {
var name: String
}
struct Person: Sendable {
var name: NSMutableString // Error: Stored property 'name' of 'Sendable'-conforming struct 'Person' has non-sendable type 'NSMutableString'
}
final class Person: Sendable {
var name: NSMutableString // Error: Stored property 'name' of 'Sendable'-conforming class 'Person' has non-sendable type 'NSMutableString'
}
もし、安全でない可能性があることを承知の上で尚コンパイルを通したい場合には、@unchecked とマークすることで警告を無視できる。
code:swift
// OK
struct Person: @unchecked Sendable {
var name: NSMutableString
}
// OK
final class Person: @unchecked Sendable {
var name: NSMutableString
}
また、Sendable への適合は、基本的には 同一ソースファイル内でのみ可能 となっている。これは、private な定義も含めた全てのプロパティが Sendable であることを確認できるのが同一ソースファイル内のみに限られるためである。もし、ソースファイル外で適合させたい場合は、@unchecked マークが必要となる。
code:SingleSourceFile.swift
struct Person { var age: Int }
// OK
// extension Person: Sendable {}
code:OtherFile.swift
// Error: Conformance to 'Sendable' must occur in the same source file as struct 'Person'
extension Person: Sendable {}
// OK
extension Person: @unchecked Sendable {}
暗黙的な Sendable への適合
Actor を用いた非同期プログラミングを行う場合、多くの enum, struct に Sendable への適合が要求される。しかし、必要になるたびに : Sendable を記述するのは手間になる。そのため、特定条件下では暗黙的に Sendable に適合する。これが行われる条件は下記の通り。
Confromance checking に成功していて、
@unsableFromInline でない internal な enum, struct である
public かつ frozen な enum, struct である
さらに、以下のいずれかである
non-generic な型である
generic な型だが、プロパティが全て Sendable に適合していることが保証されている
code:swift
// 暗黙的に Sendable
struct Person1 { var name: String }
// 暗黙的に Sendable でない
struct Person2 { var name: NSMutableString }
// 暗黙的に Sendable である
struct X<T: Sendable> { var value: T }
// 暗黙的に Sendable でない
struct Y<T> { var value: T }
既存の Hashable や Equatable、Codable 等は、いずれも汎用的な protocol ではあるが、Sendable のように暗黙的な適合は行われない。Sendable が暗黙的な適合を採用するに至った理由としては、以下が挙げられている。
Sendable はより一般的であること
(Marker Protocol であり、ランタイムに影響しないため) コードサイズ及びバイナリサイズに影響を与えないこと
Marker Protocol であり、適合することによって API が追加されないこと
Sendable である型
Sendable である型には、以下のようなものがある。
Sendable な型の Tuple は Sendable である
Metatype (Int.Type など) は Sendable である
Actor 型
KeyPath リテラル
また、Standard Libary ないの多くの struct, enum, class は Sendable に適合している。Generic type はその generic arguments が Sendable に適合していれば Sendable に適合するが、例外としては、以下がある。
ManagedBuffer
メモリ上のバッファーを操作可能な参照型であり、安全ではないため Sendable に適合すべきではない
Unsafe(Mutable)(Buffer)Pointer
必ず Sendable となる
Unsafe Pointer 型は、そもそもメモリ上への unsafe なアクセスを行うものであり、安全に利用されるかどうかはプログラマに求められる
Lazy な型は Sendable でない
arary.lazy.map {...} の戻り値など
lazy なアルゴリズムでは、non-@Sendable な closure を遅延して実行するため、安全に Sendable に適合できない
@Sendable
概要
現在の Swift では、関数型は protocol に適合できないため、Sendable にも適合できない。代わりに、@Sendable attribute を付与することができる。これにより、関数型も暗黙的に Sendable に適合したものとして扱える。
@Sendable が付与された Closure には、以下の制限がある。
mutable な変数をキャプチャできない
キャプチャする変数は Sendable である必要がある
actor-isolated にはならない (nonisolated となる) ???
WIP
エラーの扱い
関数は throws とマークすることによりエラーを送出できるが、これにより Actor isolation が壊れる可能性がある。下記のような定義を考える。Account の doSomethingRiskey() を実行すると AccountError が送出される。この時、AccountError に Account の状態を包んで送出することができてしまう。
メソッドは、引数及び戻り値の型は明示されるため、Sendable であるかどうかをコンパイル時チェックできるが、送出されるエラーの型は明示されていない。そのため、エラー型が Sendable であるかどうかをチェックすることはできず、このような問題が生じる。
code:swift
class Person {
var name: String = ""
}
struct AccountError: Error {
var person: Person
}
actor Account {
var person: Person = .init()
func doSomethingRisky() throws -> String {
throw AccountError(person: person)
}
}
これを避けるために、Error プロトコルは Sendable に適合することになった。これにより上記の問題は解決するが、既存のソースコードとの互換性が保てない問題がある。そのため、Swift 6 未満では、Error が Sendable に適合していなくとも警告にとどめられ、コンパイルエラーにはならないようになっている。
code:swift
protocol Error: Sendable { ... }
参考
SE-0302 Sendable and @Sendable closures
Protect mutable state with Swift actors - WWDC 2021