Swift Concurrency に対応していくための Strict Concurrency Check と `@preconcurrency import`

## 概要

Swift Concurrency の大きな目的の１つとしてデータ競合を防ぐことがあります。 Swift 6 からはコンパイル時のデータ競合のチェックが厳しくなり、競合を起こす可能性のあるコードはコンパイルエラーになると言われています。これに備えるため、我々が日々開発しているアプリケーションのコードも Swift Concurrency に対応していく必要があるでしょう。

Swift Concurrency への移行を一気に行うのは難しいため、徐々に移行していくための仕組みが整えられています。この記事ではその中の一部として、 Xcode 14 から Build Settings の項目に追加された `Strict Concurrency Check` と、 `import` につける `@preconcurrency` アトリビュートについて紹介します。

記事中での検証は Xcode 14 Beta 4 で行っています。

この記事では `Sendable` についてなんとなくでも理解をしていることを前提としています。もし今から理解したいと言う場合は、例えば以下のリソースがおすすめです。

- [Protect mutable state with Swift actors](https://developer.apple.com/videos/play/wwdc2021/10133)
- [Swift ConcurrencyのwithTaskCancellationHandlerとSendable](https://cockscomb.hatenablog.com/entry/2021/09/26/103128)
- [Sendable and @Sendable closures explained with code examples](https://www.avanderlee.com/swift/sendable-protocol-closures/)
- [Swift Concurrency チートシート case19 actor-boundary を越える](https://zenn.dev/koher/articles/swift-concurrency-cheatsheet#%F0%9F%92%BC-case-19-(sendable)%3A-actor-boundary-%E3%82%92%E8%B6%8A%E3%81%88%E3%82%8B)
- [SE-0302 - `Sendable` and `@Sendable` closures](https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md)

## Strict Concurrency Check

Xcode 14 から、「どのくらい厳しく Concurrency Check をするか」を表す `Strict Concurrency Check` が Build Settings に追加されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/ab62f444-5a0d-0408-8db7-84a886289d35.png)

Swift Package の場合はコンパイラフラグから設定ができます。

```Package.swift
    targets: [
        .target(
            name: "MyTarget",
            dependencies: [],
            swiftSettings: [
                .unsafeFlags([
                    "-strict-concurrency=complete"
                ])
            ]
        )
    ]
```

**2022-08-16 追記**

@omochimetaru さんに指摘いただいて `unsafeFlags` の指定方法を修正しました。また、 `unsafeFlags` を使っている target が含まれる product は他の package からバージョン指定で dependencies に追加した際にエラーが出てライブラリとして利用できなくなるので注意が必要です。より詳しくは [コメント](https://qiita.com/maiyama18/items/7f2a7c0ed4cce9d3af72#comment-29410649d0ccde6c2914) を参照ください。

**追記ここまで**

`Strict Concurrency Check` に設定する値によって、 `Sendable` に関するチェックをどこまで行うかを変えることができます。

`Strict Concurrency Check` に設定することができる値は、 `Minimal` / `Targeted` / `Complete` の３種類です。それぞれについて詳しく見ていきます。

### `Minimal`

`Minimal` は最も緩く Concurrency チェックをする設定です。最も緩いと言ってもこの設定がデフォルトで、 Swift 5.6 以前と同じレベルのチェックを行います。

**2022-08-17 追記**

@iceman5499 さんに指摘いただいたのですが、一時は `Minimal` がデフォルトでしたが、 最新では次に紹介する `Targeted` がデフォルトになっているようです。正式リリースまでに変わる可能性もある気がするのでまた改めて追記・修正します。より詳しくは [コメント](https://qiita.com/maiyama18/items/7f2a7c0ed4cce9d3af72#comment-135003a1efaddf83356c) を参照ください。

**追記ここまで**

`Minimal` では `Sendable` に準拠させようとしている型が実際には Sendale ではないことに対して警告が出ます。例えば、可変なプロパティを持つ `class` は複数の Task から同時に触られるとデータ競合を起こしうるので `Sendable` に正しく準拠することができず、以下のような警告に出ます。

```swift
final class Dog: Sendable {
    var name: String // ❗️Stored property 'name' of 'Sendable'-conforming class 'Dog' is mutable
    
    init(name: String) {
        self.name = name
    }
}
```

### `Targeted`

`Targeted` では、 `Minimal` で行われるチェックに加えて、 Swift Concurrency を使っているコードに対して Sendable チェックが有効になります。例えば、 `Task` に渡すクロージャは `@Sendable` なので `Sendable` に準拠する型しかキャプチャできませんが、 `Sendable` でない `Dog` 型のインスタンスを渡そうとすると以下のような警告が出ます。

```swift
final class Dog {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

func playWithDog(_ dog: Dog) async {
    Task {
        print("play with \(dog.name)") // ❗️Capture of 'dog' with non-sendable type 'Dog' in a `@Sendable` closure
    }
}
```

この警告により、 Swift Concurrency を利用しているコードではデータ競合の可能性がコンパイル時にわかるようになります。

手元で検証した限り、 Swift 5.6 以前で `-warn-concurrency` オプションをつけてビルドした際に走るチェックと同じレベルの厳しさが `Targeted` のようです。 Swift 5.7 では `-warn-concurrency` の振る舞いが変わって `Compelete` 相当のチェックを行うようになっていそうです。

### `Complete`

`Complete` では、 `Targeted` で行われるチェックに加えて、Swift Concurrency を使っていない箇所でも警告が出るようになります。

```swift
func playWithDog2(_ dog: Dog) {
    DispatchQueue.global().async {
        print("play with \(dog.name)") // ❗️Capture of 'dog' with non-sendable type 'Dog' in a `@Sendable` closure
    }
}
```

この `playWithDog2` は Swift Concurrency が登場する前から書くことができたので `Sendable` チェックに引っかかるのは一見不思議に思えます。しかし、実際は今までコンパイル時に検出できなかっただけで、この関数はデータ競合を発生させる可能性があります。以下のように、２つのスレッドが同時に同じインスタンスに触り、かつ一方が書き込むということが起きうるからです。

```swift
let dog = Dog(name: "John")
playWithDog2(dog)
dog.name = "Pochi" // playWithDog2 の中では別スレッドから dog を読み込むのでここの書き込みと競合し得る
```

`Strict Concurrency Check` を `Complete` にすることで、このような今までは検出できなかったデータ競合がコンパイル時にわかるようになります。

### Concurrency 関連の警告との向き合い方

１つ認識しておきたいのは、 `Strict Concurrency Check` を厳しくして警告が発生するようになっても、何かが悪くなったわけではなくそれは今現在データ競合が発生し得る箇所である可能性が高いということです。もちろん、そのことをどう受け止めて対応していくかは人やプロジェクトの性質によると思います。データ競合が起こり得ると言っても今まで普通に動いてたんだからまあ大丈夫という考え方も、早く直さないという考え方もあると思います。

いずれにしてもデータ競合の可能性がコンパイル時にわかるようになったのは純粋に素晴らしいことだと思うので、一度 `Strict Concurrency Check` を `Complete` にしてみてどういう警告が出るのかを確認してみてもいいかもしれません。

## `@preconcurrency import`

Strict Concurrency Check を `Targeted` 以上にすると、型が `Sendable` に準拠していない場合に警告が発生することがありました。その型が自分が管理している型なら何らかの対応ができますが、利用しているライブラリなど他のモジュールの型だったらどうなるでしょうか。

プロパティがすべて `Sendable` な `struct` は暗黙的に `Sendable` に準拠しますが、その `struct` が `public` な場合は明示的に準拠させないといけなくなります。これは `public` な型はモジュールのインターフェースであり、インターフェースが特定のプロトコルに暗黙的に準拠するのは互換性の観点から好ましくないからだと思います。そのため、基本的にモジュールの作者が Concurrency に対応しようと思わない限りモジュールの型が `Sendable` になることはありません。また、ある型を `Sendable` に準拠させることができるのは型が定義されているのと同じファイル内でないといけないという制限があるため、利用側で `Sendable` に準拠させることはできません。そのため、 `Targeted` 以上の設定で警告を消すには、基本的にライブラリの作者が Concurrency に対応してくれるのを待つしかないと思います。もちろん `Minimal` にすれば警告は消えますが、そうすると自分が管理している型の `Sendable` チェックまで効かなくなってしまいます。

このような状況を解決するために用意されているのが `@preconcurrency import` です。以下のような状況を考えます。 `AnimalKit` に定義されている `Dog` は `Sendable` に準拠していないため、 `App` 側で警告が出ています。そして、 `App` 側で `Dog` を `Sendable` に準拠させることはできません。

```swift
// -- AnimalKit module --

public final class Dog {
    public var name: String
    
    public init(name: String) {
        self.name = name
    }
}

// -- App module --

import AnimalKit

func playWithDog(_ dog: Dog) async {
    Task {
        print("play with \(dog.name)") // ❗️Capture of 'dog' with non-sendable type 'Dog' in a `@Sendable` closure
    }
}
```

ここで `@preconcurrency` をつけて `import` することで、 `import` 先のモジュールの型に由来する `Sendable` 周りの警告が出なくなります。これにより、利用しているライブラリが Concurrency 対応するまでの間、そのライブラリが原因の警告だけを選択的に見ないようにすることができるというわけです。

```swift
// -- App module --

@preconcurrency import AnimalKit

func playWithDog(_ dog: Dog) async {
    Task {
        print("play with \(dog.name)") // ❗️Capture of 'dog' with non-sendable type 'Dog' in a `@Sendable` closure
    }
}
```

ライブラリ側で Concurrency 対応が完了すると、 `@preconcurrency` を外すことができるようになります。 Concurrency 対応の結果としては、 `Dog` が `Sendable` に準拠するようになる場合と、逆に `Dog` が `Sendable` でないことが確定する場合の２通りあります。

まずは `Dog` が `Sendable` に準拠する場合を考えます。 `AnimalKit` の作者が、 `name` はインスタンスを生成した後に変更することはないので `var` ではなく `let` で定義できることに気づくとします。そうすると `Dog` はイミュータブルになるので `Sendable` に準拠できるようになります。

```swift
public final class Dog: Sendable {
    public let name: String
    
    public init(name: String) {
        self.name = name
    }
}
```

この変更を取り込むことで `@preconcurrency` は不要になりますが、それに `AnimalKit` の利用者が気づけるように Xcode が今度は `@preconcurrency` 側に警告を出してくれます。

```swift
@preconcurrency import BookKit // '@preconcurrency' attribute on module 'BookKit' is unused

func playWithDog(_ dog: Dog) async {
    Task {
        print("play with \(dog.name)")
    }
}
```

この警告のおかげで無駄な `@preconcurrency` が残り続けることは考えづらいので、個人的には必要なら `@preconcurrency` を躊躇なく使っていってよいのではないかと感じています。もちろん、定期的に `@preconcurrency` で `import` しているモジュールを洗い出して Concurrency の対応状況をチェックするなどの活動は必要だと思います。

`AnimalKit` の Concurrency 対応のもう一つの結果として、 `Dog` が `Sendable` でないことが確定する場合も考えられます。[プロポーザルの記述](https://github.com/apple/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md#sendable-conformance-status) を読む限り、将来的に Swift 6 でコンパイルするようになれば明示的に `Sendable` に準拠しない型は全て `Sendable` でないと判断されるようになるはずですが、 Swift 5 の時点では利用しているライブラリの型が `Sendable` でないと確定するのは

- 型が `Sendable` に準拠しているが、available でないとき
- 型が `Sendable` に特定の条件下で準拠しているが、その条件が満たされていないとき

の２通りです。検証のために簡単に `Sendable` でない型を作るには、以下のように書いて前者の条件を満たすとよいでしょう。

```swift
public final class Dog {
    public let name: String
    
    public init(name: String) {
        self.name = name
    }
}

@available(*, unavailable)
extension Dog: Sendable {}
```

`AnimalKit` 側でこのように `Dog` が `Sendable` でないことが確定した場合は、以下のように `App` 側で `@preconcurrency` を無視して警告が復活します。

```swift
@preconcurrency import BookKit

func playWithDog(_ dog: Dog) async {
    Task {
        print("play with \(dog.name)") // ❗️Capture of 'dog' with non-sendable type 'Dog' in a `@Sendable` closure
    }
}
```

このように、 `Dog` が `Sendable` でないことが確定した場合でもモジュールの利用者は気づくことができるようになっています。

## まとめ

- Xcode 14 から Build Settings に追加された `Strict Concurrency Check` により Concurrency チェックの厳しさを変えられる
    - `Minimal` : `Sendable` への準拠を宣言した型が実際には Sendable でない時に警告が出る。 Swift 5.6 までの挙動と同じ
    - `Targeted` : Swift Concurrency を利用しているコードでデータ競合の可能性がある箇所で警告が出る
    - `Complete` : Swift Concurrency を利用していないコードも含めてデータ競合の可能性がある箇所で警告が出る
- `Targeted` 以上の厳しさでは、 `import` している他のモジュールが Concurrency 対応していないせいで警告が出てしまうことがある。これを `@preconcurrency` で抑制することができる

## 参考

- [Eliminate data races using Swift Concurrency | WWDC22](https://developer.apple.com/videos/play/wwdc2022/110351/)
- [SE-0337 - Incremental migration to concurrency checking](https://github.com/apple/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md)
- [SE-0302 - `Sendable` and `@Sendable` closures](https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md)