Strict Concurrency対応をActorによるmutable Stateを保護する事例で理解する


本記事は、[フューチャーアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/future)の16日目です。

# はじめに
HealthCare Innovation Group(HIG)[^1]所属の橋本です。


2024年9月16日にXcode 16がリリースされました！
Xcode16より内蔵されているSwift 6より、データ競合のチェックが厳しくなり、データ競合が起こる可能性のあるコードが含まれていると、コンパイル時にエラーとなってしまいます。

今回は、`Actor`用いてmutable Stateを保護する事例をとって、Swift 6移行に向けて実際にどのような対応が必要になっていくるのかを見ていきます。

:::note info
以下の記事も[Swift Advent Calendar 2024](https://qiita.com/advent-calendar/2024/swift)の8日目としてフューチャー技術ブログに投稿していますので、Swift 6移行対応に興味のある方はぜひ、覗いてみてください。


- [Swift6移行に向けて、Isolation domainとSendableを理解する](https://future-architect.github.io/articles/20241208a/)

:::



## 環境

- OS: macOS Sequoia 15.1.1（24B91）
- Xcode: Version 16.2 (16C5032a)
- Swift: 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)




本記事では、Swift6への移行準備する想定の記事となっており、以下の設定とする。

```
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
```

それぞれ、TARGETS > Build Settings から設定ができます。

- `SWIFT_STRICT_CONCURRENCY = complete`の設定箇所


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3506730/968a1070-a3ae-0cdc-7848-89c224db8d99.png)

- `SWIFT_VERSION = 5.0`の設定箇所

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3506730/cba15d1e-8146-41a9-a626-9f277fa1fcd0.png)



# データ競合が起こる事例に対して、どのように解決していくか

今回は、WWDC2021の次のセッション内容をもとにデータ競合を防ぐ（=Strict Concurrecy対応）方法をみていきます。

https://developer.apple.com/videos/play/wwdc2021/10133/

データ競合が起こるときは、上記のセッションで次のように説明されています。


>Data race occer when
・Two Thread concurrenclently access the same data
・One of them is a write 

つまり、データ競合は
- 同じデータに、２つのスレッドから平行にアクセスする
- アクセスするときに、少なくとも１つが書き込みである


まずは、データ競合が起こる例を示す。

```swift:修正前のコード
class Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}

func run() {
    let counter = Counter()

    Task.detached {
        print(counter.increment()) // Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race; this is an error in the Swift 6 language mode
    }

    Task.detached {
        print(counter.increment()) // Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race; this is an error in the Swift 6 language mode
    }
}
```

上記のようなコードを書くと、次のような警告メッセージがでます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3506730/26ab8be6-d957-cf39-c70a-841d6e694075.png)

> Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race; this is an error in the Swift 6 language mode

> (日本語訳)	非Sendable型 @isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' の値が転送後にアクセスされました。後続のアクセスで競合が発生する可能性があります。これはSwift 6言語モードではエラーとなります。


このコードだと、「Swift 6モードへあげたとき、エラーになってしまいますよ」と教えてくれています。これが`SWIFT_STRICT_CONCURRENCY = complete`による効果です。このように、Swift 5モードで`SWIFT_STRICT_CONCURRENCY = complete`とすることでSwift6移行に向けて、改善しないといけない箇所を洗い出すことができます。


# Actorを用いてデータ競合を解決する

次のWWDC2021のセッション内容を参考にしています。

https://developer.apple.com/videos/play/wwdc2021/10133/


## Actorとは
`Actor`は共有可変状態のための同期メカニズムです。すなわち、`Actor`は`Actor`インスタンスごとに残りのプログラムから隔離することで、データへの同期アクセスを実現しています。
`Actor`インスタンスごとに隔離することができる（Isolation domain）を持ち、隔離したIsolation domain内のデータにアクセスをするには、Actorを経由するため、並行にアクセスをされないことを`Actor`が保証しています。

`Actor`は、`Class`と同様に参照型で、Swiftにおける型と同様の機能が提供されています。プロパティ、メソッド、イニシャライザ、サブスクリプトの機能を持ち、プロトコルに準拠させたり、エクステンションで拡張することもできます。

https://developer.apple.com/documentation/swift/actor

詳細は、次のProposal: [SE-0306](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0306-actors.md)を確認してみてください。

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0306-actors.md


## Actorを使ってmutable Stateを保護する
さきほどのデータに競合が起こる可能性のあるコードをActorを用いて、改善していきます。

```diff_swift
- class Counter {
+ actor Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}
```

`Counter`を`actor`として、定義することで`value`プロパティへ並列にアクセスされることから保護することができます。これは、`actor`が`Sendable`に準拠しているため、並行タスクの間で安全に共有が可能になっています。


`class`を`actor`に変えたことにより、次のエラーが発生します。

> Expression is 'async' but is not marked with 'await'

これは、`actor`内のメソッドは非同期メソッドとして扱われるため、呼び出し時に`await`をつけずに呼び出しているためエラーとなっています。


```swift
func run() {
    let counter = Counter()

    Task.detached {
        print(counter.increment()) // Expression is 'async' but is not marked with 'await'
    }

    Task.detached {
        print(counter.increment()) // Expression is 'async' but is not marked with 'await'
    }
}
```

エラーメッセージの内容に従って、`await`をメソッドにつけます。

```diff_swift
func run() {
    let counter = Counter()

    Task.detached {
-        print(counter.increment()) // エラー発生
+        print(await counter.increment()) 
    }

    Task.detached {
-        print(counter.increment()) // エラー発生
+        print(await counter.increment()) 
    }
}
```

以上で、`Swift Concurrency Checking`の警告メッセージは消え、Swift 6でもエラーが出ない形に修正が完了しました。



```swift:修正後のコード全体
actor Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}

func run() {
    let counter = Counter()

    Task.detached {
        print(await counter.increment())
    }

    Task.detached {
        print(await counter.increment())
    }
}
```



# さいごに

今回は、一つの`Actor`用いてmutable Stateを保護する事例をとって、Swift 6移行に向けて、実際にどのように対応するかを振り返りました。

データ競合が起こり得る箇所は、コンパイル時に潰すことができるようになるのは最終的には意図しないクラッシュや不具合等の数を大きく減らすことができる期待できるため、ユーザーの離脱を減らすことにつながるため、ビジネス的にも大変有用だと考えています。

Swift 6移行に取り組む際の参考になれば幸いです。




[^1]: 医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは[未来報](https://note.future.co.jp/n/n8b57d4bf4604)の記事をご覧ください。