[Swift] actor reentrancy のありがたみと注意点

## 概要

Swift Concurrency の actor は可変な状態をデータ競合から守るためのコンポーネントです。Swift の actor の特徴として reentrant （再入可能）であることが挙げられます。これがなんのことで、どういうありがたみがあって、アプリ開発者として気をつけるべき注意点にどういうものがあるのかを、主に [Swift Evolution の actor のプロポーザル](https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md) をもとにまとめます。

前提として、自分は Swift Concurrency 以外の文脈における actor のことはわかっていません。また、 WWDC / プロポーザルでは reentrant / reentrancy という言葉が使われていますが、記事中ではそれぞれ日本語で再入可能 / 再入と言うことにします。

この記事中での動作検証は Xcode 14 Beta 3 で行っています。

## actor reentrancy（再入）とは

actor が再入可能であるとは、 actor のメソッドが実行中に `await` で suspend したときにその actor の他のメソッドの実行を開始できることを言います。例えば、以下のコードを実行すると、再入のため `a` → `b` → `c` ではなく `a` → `c` → `b` の順で出力されます [^1] 。

[^1]: もちろん実行環境によっては `a` → `b` → `c` と実行されることが絶対にないとは言えないと認識していますが、この記事ではそういう可能性は考えないことにします。

```swift
actor MyActor {
    func f1() async {
        print("a")
        try! await Task.sleep(nanoseconds: 2_000_000_000)
        print("b")
    }
    
    func f2() async {
        print("c")
    }
}

let myActor = MyActor()

Task.detached {
    await myActor.f1()
}

Task.detached {
    try! await Task.sleep(nanoseconds: 1_000_000_000)
    await myActor.f2()
}
```

具体的に実行順を追っていくと以下のようになります。

- まず `f1` が実行されて `a` が出力され、その後２秒間 sleep する
- 一方で、 `f1` が実行された１秒後に `f2` が呼ばれ、 `f2` は `f1` の完了を待たずに実行開始されて `c` が出力される
- `f1` の sleep が完了し、 `b` が出力される

以上の流れの中の、 `f1` が実行完了していないにも関わらず `f2` が実行開始される部分が actor の再入です。仮に再入が許されないと仮定すると `f1` が完了するまでは `f2` の処理を開始できないので `a` -> `b` -> `c` と出力されますが、現状の Swift の actor はそのような仕様にはなっていないということです。

注意点として、再入が起こるからといって１つの actor 上で「同時」に２つの処理が実行されることがあるわけではありません。再入が起こり得るのはあくまで actor の処理が `await` で suspend した場合のみだからです。上記の例では `f1` の中の sleep によって actor の手が空いたためその間に `f2` を実行しているだけで、 `f1` の処理を同期的に実行している最中に `f2` が開始されることはありません。また、仮に `f2` が実行中に `f1` の sleep が終わったとしても `f1` はすぐに続きの処理を実行し始めるのではなく、 `f2` の実行完了を待ちます。以上により actor が持つ状態が複数の処理から同時に触られるということはないので、再入が起きた上でも actor は状態をデータ競合から守ることができます。

## 再入のありがたみ

原理上は再入を許さない actor というのも考えられますが、現状の Swift では再入可能な actor しかサポートされていません。これは、再入がいくつかの問題を排除してくれるというメリットがあるためです。もし再入が許されていなかったらどうなるかという仮定のもとにこれらの問題を見ていきます。

### 再入が防いでくれる問題１：デッドロック

以下の２つの actor が登場する処理について、仮に actor が再入不可能だったらどうなるかを考えてみましょう。 再入不可能だとすると、 actor のあるメソッドが実行開始されたらたとえ suspend 中であっても別のメソッドを実行開始できないということになります。

```swift
// 再入不可能だと仮定する
actor User {
    func startWorking(with mac: Mac) async {
        await mac.launch(user: self)
    }
    
    func useTouchID() {
        print("place finger")
    }
}

actor Mac {
    func launch(user: User) async {
        await user.useTouchID()
        print("launch completed")
    }
}
```

`User` の `startWorking` を実行すると、

- `startWorking` が `Mac` の `launch` を呼び出したとき `Mac` は `User` の `useTouchID` を呼び出そうとする
- `User` の `startWorking` が実行中なので、 再入不可能な `User` は別の処理である `useTouchID` を開始することができない
- これにより `Mac` は `User` の `startWorking` の完了を待ち続けるが、逆に `User` は `startWorking` の完了のために `Mac` の `launch` の完了を待ち続けている

という流れでデッドロックが発生し、処理が進まなくなってしまいます。

もちろん実際には Swift の actor が再入可能なので、 `userTouchID` は実行することができ、これにより `Mac` の `launch` は完了、 `User` の `startWorking` も完了します。このように、 actor が再入可能であるおかげでデッドロックが防がれています。

再入不可能であることによるデッドロックは実行時に検出したり、すべての actor の処理にタイムアウトを設定することで解除することができます。ただし、 actor のプロポーザルでは、そのような手法はコストが高いし協調的なキャンセルとの相性が悪く Swift Concurrency の方向性には合わないと判断されたと書かれています。

### 再入が防いでくれる問題２：ブロッキング

再入ができない actor の場合、並行して呼び出された場合にブロックされてしまいパフォーマンスが悪くなるという問題があります。プロポーザルで使われている以下の例で考えてみます。仮に `ImageDownloader` が再入不可能とすると `image1` の呼び出しが先に行われた場合に、そのダウンロードとデコードが完了するまでは `image2` の方の `getImage` を開始することができず待たされることになります。結果として、ほぼ同時に呼ばれた２つの image の取得に、１つの image を取得する場合のほぼ２倍の時間がかかってしまいます。

```swift
// 再入不可能だと仮定する
actor ImageDownloader {
    var cache: [URL: Image] = [:]
    
    func getImage(_ url: URL) async -> Image {
        if let cachedImage = cache[url] {
            return cachedImage
        }
        
        let data = await download(url)
        let image = await Image(decoding: data)
        return cache[url, default: image]
    }
}

let downloader = ImageDownloader()
Task.detached { let image1 = await downloader.getImage(url1) }
Task.detached { let image2 = await downloader.getImage(url2) }
```

再入が可能であることにより、 `image1` の呼び出しが `await download(url)` まで到達して suspend した隙に `image2` の方でもダウンロードが開始でき、並行にダウンロード処理を行うことができます。これにより、２つの image のダウンロードが再入が許されない場合より速く完了します。

## 再入の注意点

再入が防いでくれる問題を２つ挙げましたが、逆に再入による注意点として suspend の前後で actor の状態が変わっている可能性がある点が挙げられます。このことに気づかず、actor の suspend 前の状態が維持されている前提でコードを書いてしまうと予期しない挙動になることがあります。
例として以下のカウンターを考えます。

```swift
actor Counter {
    var count: Int = 0
    
    func increment() -> Int {
        count += 1
        return count
    }
}
```

`increment` は内部状態である `count` を増加させて返します。このコードは想定通り動き、`increment` は呼ぶたびに１ずつ増えた値が返ってきます。
ここで仕様変更があって、 `increment` 処理の値を増やしてから値を返すまでの間になんらかの非同期処理をする必要が出てきたとします。ここでは一旦 `sleep` にしておきましょう。

```swift
actor Counter {
    var count: Int = 0
    
    func increment() async -> Int {
        count += 1
        try! await Task.sleep(nanoseconds: 1_000_000_000)
        return count
    }
}
```

この変更によって`increment` を呼ぶたびに値が１ずつ増えて返ってくるという振る舞いは壊れています。以下の３つの `increment` 呼び出しでは `1` `2` `3` が出力されることを期待したくなりますが、実際にはすべて `3` が出力されます。

```swift
let counter = Counter()

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

これは、１つ目の呼び出しが `count` を増やして `sleep` に入った際に再入により２つ目の呼び出しが実行され `count` が増え `sleep` に入り、３つ目の呼び出しも同様に `count` を増やすので、３つの呼び出しの `sleep` が終了する際には `count` が `3` になっているためです。

このように、suspend を挟むと再入によって actor 内部の状態が変化していることがあるので、`await` の後で `await` 前の状態が維持されていない可能性があることを想定した実装をする必要があります。 actor の処理の中に `await` があったらその時点で変なことが起こらないか意識しておくくらいの心持ちでいたほうがいい気がします。

逆に言うと、仕様変更前の `Counter` のように `await` がなければそもそも再入が発生しないので再入の影響を考える必要はありません。[Protect mutable state with Swift actors](https://developer.apple.com/videos/play/wwdc2021/10133/) では actor の状態の変更は同期的な処理に閉じ込めることが推奨されています。

## まとめ

- Swift の actor は再入可能。これは actor の処理が suspend した時に、同じ actor で別の処理を開始できることを意味する
- 再入可能であるおかげで、actor 処理におけるデッドロックやブロッキングが防がれている
- 再入により suspend 前後で actor の状態が変わり得ることに注意した実装をしていく必要がある

## 参考

- [SE-0306 - Actors](https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md)
- [Protect mutable state with Swift actors | WWDC21](https://developer.apple.com/videos/play/wwdc2021/10133/)