Sendable理解した

# はじめに
Swift6が登場した現在のiOS開発において、`Sendable`というプロトコルを見かける頻度が爆増しました。
`Sendable`調べたこと何回かあるけど、毎回ふわっとしか理解せず次見かけた時「なんだっけこれ、、」と何度も調べる羽目になっていたので、これを機に完全に理解したと思う心意気でキャッチアップしました。
ので今回はそのキャッチアップした情報を記事にしてみました。

:::note info
なるべくコードベースで確認して情報の正当性を担保することを心がけていますが、説明や解釈の誤りなどございましたらご指摘いただけますと幸いです。
:::

# `Sendable`とは
protocolであり、`Sendable`に準拠することでデータ競合が起きないということを示す型です。
つまり`Sendable`に準拠した構造体やクラス、enumなどはデータ競合が起きないことがコンパイル側で保証してくれるのです。

と解説されている記事はおそらく山のようにありますし、
私はこの解説では最初ちんぷんかんぷんでしたので、もう少し深掘ります。

`Sendable`がある時にどんな困り事が解決されているのかが、分かれば`Sendable`の意味も腑に落ちると考えているので、どんな困り事が解決されているのか考えてみましょう。

先ほどの説明を読むと、___データ競合___ が起きないとあるので、___データ競合___ がなんなのかが理解できれば`Sendable`のイメージがつかめる気がします！

### データ競合とは？

まずはWikipediaでデータ競合の意味を見てみます。

https://ja.wikipedia.org/wiki/%E3%83%87%E3%83%BC%E3%82%BF%E7%AB%B6%E5%90%88

>マルチスレッドのプログラムにおいて、データ競合は次の条件がすべて満たされた場合に発生する。
>
>1つのプロセス内の2つ以上のスレッドがメモリ上の同じ場所に同時にアクセスする。
>そのアクセスの少なくとも1つが書き込みアクセスである。
>どのスレッドも、排他的ロックを使用して、そのメモリへのアクセスを制御していない。

定義だけ見てもイメージしづらいので、実際のコード競合が起きうるコードを見てみましょう。
以下コードではデータ競合を発生させる可能性があります。

```swift
class Foo {
    
    var value = ""
}

let foo = Foo() // @MainActorの変数

@MainActor
func main() {
    
    DispatchQueue.global().async {
        
        foo.value = "Hello, World! on DispatchQueue.global" // ①
        // Main actor-isolated let 'foo' can not be referenced from a nonisolated context
    }

    Task.detached {
    
        await foo.value = "Hello, World! on Task" // ②
        // Non-sendable type 'Foo' in implicitly asynchronous access to main actor-isolated let 'foo' cannot cross actor boundary
    }

    Task.detached {
        
        await print(foo.value) // ③
        // Non-sendable type 'Foo' in implicitly asynchronous access to main actor-isolated let 'foo' cannot cross actor boundary
    }
}

main()
```

上記例では、以下複数のスレッドで`foo`に対して非同期で読み書きを行なっています。
- ①`DispatchQueue.global().async`のブロックで書き込み
- ②`Task.detached`のブロックで書き込み
- ③`Task.detached`のブロックで読み込み

また上記の`foo`は排他制御(読み書きを行なっている間に他の書き込みが行えない様にする)を行なっていません。

よって、データ競合の定義にある条件を全て満たしており、データ競合が起きうる可能性があります。


### データ競合が起きると何が起きるのか
データ競合が起きた場合の結果は他の言語でもそうだと思いますが、保証されていないとされています。
最悪の症状として下記にもあるとおり、常に失敗せず成功することもあるという予測不能な動作が起こることです。
この特性により、潜在的な問題を抱えたままテストで顕在化せずユーザーの元へリリースされてしまうということがしばしば起こります。

>従来、可変状態は、慎重な実行時同期によって手動で保護する必要がありました。ロックやキューなどのツールを使用すると、データ競合の防止は完全にプログラマー次第でした。これは、正しく行うだけでなく、長期間にわたって正しく維持することも非常に困難です。同期の必要性を判断することさえ難しい場合があります。最悪なのは、アンセーフ コードでは実行時に失敗が保証されないことです。このコードは、データ競合の不正確で予測不可能な動作特性を示すために非常に異常な条件が必要であるため、多くの場合は機能しているように見えることがあります。
>
>より正式には、データ競合は、あるスレッドがメモリにアクセスしているときに、別のスレッドが同じメモリを変更しているときに発生します。Swift 6 言語モードでは、コンパイル時にデータ競合を防ぐことで、これらの問題が解消されます。

https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/dataracesafety/

上記説明でもわかるとおり、これまでデータ競合による不具合を検知することが難しかったのですが、swift6ではこれがデータ競合が起こり得るコードはコンパイルエラーとなり未然に防ぐことができる様になっています。
これは`Sendable`に準拠した型以外が、スレッドを跨いで参照されているかどうかを見て、もし跨いでいた場合はコンパイルエラーとする仕様になっているためです。

`Sendabale`がデータ競合が起きないことを保証してくれているので、逆に`Sendabale`に準拠していない型がスレッドを跨ぐといったデータ競合を起こしうる実装をブロックすることでデータ競合を静的解析で防いでいるのです。

ここまでわかると`Sendable`の意味が見えてくるのではないでしょうか。

## Sendableに準拠するには
`Sendable`の意味はわかったので、続いてどうすれば準拠できるのか見ていきます。

準拠する対象が値型、列挙型か参照型かによって、準拠するための条件が変わってくるため分けて考えていきます。

### 値型、列挙型が`Sendable`に準拠する方法

以下が準拠するための条件です。
- プロパティ(enumの場合はAssociated Value)が全て`Sendable`に準拠していること

以下は`Sendable`に準拠できている例です
```swift
struct Bar: Sendable {
    
    var value: String // Stringが`Sendable`に準拠している
}

enum Hoge: Sendable {
    
    case a(Bar) // Barが`Sendable`に準拠している
}
```

以下は`Sendable`に準拠できていない例です
```swift
struct Bar: Sendable { // Stored property 'value' of 'Sendable'-conforming struct 'Bar' has non-sendable type 'Foo'
    
    var value: Foo // プロパティのvalueが`Sendable`に準拠していない
}

enum Hoge: Sendable { // Associated value 'a' of 'Sendable'-conforming enum 'Hoge' has non-sendable type 'Foo'
    
    case a(Foo) // Associated valueが`Sendable`に準拠していない
}

class Foo {
    
    var value = ""
}
```

値型、列挙型は上記の条件を意識せずとも、気づいたら`Sendable`に準拠していたということもあります。
というのも、値型、列挙型は上記の条件を満たしている場合は`Sendable`に暗黙的に準拠するという仕様のためです。

以下のように、`Sendable`を明示的に記載して準拠させずとも、勝手に`Sendable`に準拠しています。
```swift
struct Bar { // 暗黙的に`Sendable`に準拠している
    
    var value: String // Stringが`Sendable`に準拠している
}

enum Hoge { // 暗黙的に`Sendable`に準拠している
    
    case a(Bar) // Barが`Sendable`に準拠している
}
```

しかし、モジュールを跨いだ参照になると暗黙的な`Sendable`は効かず、参照している側(importしている側)からは`Sendable`には見えないという仕様があるので、そのときは明示的に`Sendable`に準拠させる必要があります。

### 参照型が`Sendable`に準拠する方法

以下が準拠するための条件です。
- プロパティが全て`Sendable`に準拠していること
- プロパティが全て定数であること(※)
- `final`なclassであること

:::note info
補足
※「プロパティが全て定数であること」の条件には例外もあります。
`actor`であれば、変数のプロパティを保持していても暗黙的に`Sendable`に準拠しているので、「プロパティが全て定数であること」を満たす必要はありません。

https://t.co/br7aXPg93m

:::

以下は参照型が`Sendable`に準拠している例です
```swift
final class Foo: Sendable {
    
    let value = ""
}
```

以下は参照型が`Sendable`に準拠できていない例です

`Sendable`に準拠していないプロパティがある例
```swift
class Foo: Sendable { // Non-final class 'Foo' cannot conform to 'Sendable'; use '@unchecked Sendable'
    
    let value = ""
}
```

プロパティが定数でない例
```swift
final class Foo: Sendable {
    
    let value = ""
    let value2 = Boo() // Stored property 'value2' of 'Sendable'-conforming class 'Foo' has non-sendable type 'Boo'
}

class Boo {
    
    let value = ""
}
```

finalがない例
```swift
class Foo: Sendable { // Non-final class 'Foo' cannot conform to 'Sendable'; use '@unchecked Sendable'
    
    let value = ""
}
```

上記で見て取れるように、参照型は値型や列挙型よりも`Sendable`に準拠するための条件が多いです。
一つ目の ___プロパティが全て`Sendable`に準拠していること___ という条件は値型や列挙型と同じですが、その他の制約がなぜ必要かも見ていきます。

#### 「プロパティが全て定数であること」が`Sendable`に必要な理由
これは、参照型の他の変数に渡してもその変数には同じインスタンスが保持される特性に起因していると考えています。

以下ケースのように、`Task.detached`で`foo`を別スレッドで書き込みを行う際に、キャプチャリストにて`foo`をキャプチャしても同じインスタンスのためデータ競合の可能性が出てきます。
```swift
final class Foo {
    
    var value = ""
}

func main() {
    
    let foo = Foo() // ①

    Task.detached {
        
        foo.value = "Hello, World! on Task"
    }
    
    Task.detached { [foo] in // ①と同じインスタンスのfoo

        let foo = foo // ①と同じインスタンスのfoo
        foo.value = "Hello, World! on Task2"
    }
    
    foo.value = "Hello, World! on main"
}

main()
```

そのため、`class`のような参照型をデータ競合が発生しないことを担保するためには、
保持しているプロパティは変更できないようにする必要があります。

これが`struct`といった値型であれば、キャプチャリストにてキャプチャするとクロージャ内では別インスタンスを参照することになるためスレッドを跨いで参照してもデータ競合は防がれます。
```swift
struct Bar: Sendable {
    
    var value: String
}

func main() {
  
    var bar = Bar(value: "") // ①

    Task.detached {
        
        bar.value = "Hello, World! on Task"
    }
    
    Task.detached { [bar] in // ①とは異なるインスタンス
        
        var bar = bar // ①とは異なるインスタンス
        bar.value = "Hello, World! on Task2"
    }
    
    bar.value = "Hello, World! on main"
}

main()
```

#### 「`final`なclassであること」が`Sendable`に必要な理由
こちらは過去に以下記事を書きましたので、よかったらご参照ください。

https://qiita.com/stotic-dev/items/bb719b19707747b15feb

上記の他に値型、列挙型、参照型関係なく、例外的に制約なしに強制的に`Sendable`に準拠させる手段もあります。
それが、`@unchecked Sendable`を使用することです。

```swift
class Foo: @unchecked Sendable {
    
    var value = ""
}
```

この方法はコンパイラが`Sendable`に準拠できているかというチェックをスキップして`Sendable`に適合できますが、その性質上コンパイラがデータ競合を起きないことを保証しているわけではありません。
そのため`@unchecked Sendable`を使用する場合は、開発者自身がデータ競合が起きないことを保証する必要があるので、使用には注意が必要です。

例えば、以下の様にキューで保護してデータ競合が起きないことが開発者にはわかるが、コンパイラにはわからない場合だと`@unchecked Sendable`を使用するのが有効と考えています。
```swift
final class Foo: @unchecked Sendable {
    
    private var _value = ""
    private let executeQueue = DispatchQueue(label: "Foo.executeQueue")
    
    var value: String { // valueを取得するときはシリアルキューを介して読み取る
        
        get async {
            
            await read()
        }
    }
    
    func update(_ value: String) async {
        
        return await withCheckedContinuation { continuation in
            
            executeQueue.async(flags: .barrier) { // valueの値を書きこむときは、シリアルキューを介して、他の_valueへのアクセスが終わってから書き込みを行う
                
                self._value = value
                continuation.resume()
            }
        }
    }
    
    private func read() async -> String {
        
        return await withCheckedContinuation { continuation in
            
            executeQueue.async {
                
                continuation.resume(returning: self._value)
            }
        }
    }
}
```

以上が、`Sendable`に準拠する方法でした。

## Swift Concurrencyにおいて`Sendable`であるとどんなことができるのか
swift concurrencyでは同時アクセスから保護してくれる領域があります。
これをisolation domain(※)といったりするそうで、主に以下３種類があります。

- Global Actorに隔離されている領域
- Actorに隔離されている領域
- Actorに隔離されていない領域

これらの領域間を跨ぐことをisolation baundaryと呼び、swift concurrencyでは`Sendable`でない型がこれを行うことができない様になっています。
この仕様は上記で解説したデータ競合を防ぐためです。

以下は、MainActorからnonisolated(Actorに隔離されていない領域)へisolation baundaryしている例です。
例の通り、`Sendable`に準拠した型はMainActorからnonisolatedへ渡す(isolation baundary)ことができます。

```swift
class NonSendableObject {
    
    var value = ""
}

struct SendableObject: Sendable {
    
    var value: String
}

@MainActor
func main(sendableObject: SendableObject, nonSendableObject: NonSendableObject) async {
    
    await sendNonIsolatedDomain(sendableObject) // MainActorからnonisolatedへSendableの型を渡す
    await sendNonIsolatedDomain(nonSendableObject) // MainActorからnonisolatedへSendableでない型を渡す　→　❌コンパイルエラー
}

nonisolated
func sendNonIsolatedDomain(_ value: SendableObject) async {
    
    print("sendNonIsolatedDomain: \(value)")
}

nonisolated
func sendNonIsolatedDomain(_ value: NonSendableObject) async {
    
    print("sendNonIsolatedDomain: \(value)")
}

Task { // MainActor
    await main(sendableObject: SendableObject(value: ""),
               nonSendableObject: NonSendableObject())
}
```

【参考】
- https://www.youtube.com/watch?v=AUcn2y2jjNs
- https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/

# おわり
`Sendable`について、概念の意味や意義、使用方法を整理してみました。
ご参考になる方がいらっしゃれば幸いです。