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 {
func randomFriend() -> Person?
}
let account = Account()
if let friend = await account.randomFriend() {
friend.name = "hoge" // actor 外部で 状態が変更できてしまう!!
}
概要
code:swift
@_marker
protocol Sendable {}
ある型について、その public な API が並列に実行されても安全である (≒ Data race を生じさせない) ことが保証されているのであれば、Sendable に適合することを検討できる。値型はもちろん、内部的に Lock による同期機構を持った class や、mutator を持たない immutable な class なども、Sendable への適合を検討して良い。 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 {}
Actor を用いた非同期プログラミングを行う場合、多くの enum, struct に Sendable への適合が要求される。しかし、必要になるたびに : Sendable を記述するのは手間になる。そのため、特定条件下では暗黙的に Sendable に適合する。これが行われる条件は下記の通り。 Confromance checking に成功していて、
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 }
また、Standard Libary ないの多くの struct, enum, class は Sendable に適合している。Generic type はその generic arguments が Sendable に適合していれば Sendable に適合するが、例外としては、以下がある。 ManagedBuffer
メモリ上のバッファーを操作可能な参照型であり、安全ではないため Sendable に適合すべきではない Unsafe(Mutable)(Buffer)Pointer
Unsafe Pointer 型は、そもそもメモリ上への unsafe なアクセスを行うものであり、安全に利用されるかどうかはプログラマに求められる
arary.lazy.map {...} の戻り値など
概要
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 { ... }
参考