[Swift] actor の init / deinit における decay を理解する

## 概要

actor の `init` / `deinit` は初見では不可解に思えるような振る舞いをします。例えば、以下のコードでは２回 `print(self.count)` を呼んでいますが２つ目だけに警告が出ます。

```swift
actor Counter {
    var count: Int

    init(count: Int) {
        self.count = count

        print(self.count)
        print(self)
        print(self.count) // ❗️Cannot access property 'count' here in non-isolated initializer; this is an error in Swift 6
    }
}
```
この警告は actor が持つ可変な状態をデータ競合から守るために設定されている decay という仕組みに由来するものです。この記事では、上記の例も含めて actor の `init` / `deinit` の振る舞いを理解していきます。

記事中での動作検証は Xcode 14 Beta 4 で行っています。また、 将来的に Swift 6 でエラーになるコードに対して警告が出るように Strict Concurrency Check を `Complete` に設定しています。 Strict Concurrency Check の詳細については以下の記事を参照してください。

https://qiita.com/maiyama18/items/7f2a7c0ed4cce9d3af72

## TL;DR

- actor の sync な `init` の中では、 `self` を別の処理の引数に渡すなど特定の操作をした以降に non-isolated なコンテキストに切り替わる decay が発生する
- async な `init` では decay が発生しない代わりに `self` のすべてのプロパティが初期化された時点で暗黙的に suspend し自身の actor コンテキストに切り替わる
- actor の deinit では decay に加えて、decay が発生する前でも `Sendable` でないプロパティにはアクセスできないという制限が加わる

## sync な init における decay

Swift の `init` は async にすることもできますが、まずは sync な `init` から見ていきます。例として以下の actor を考えます。

```swift
actor Counter {
    var count: Int

    init(count: Int) {
        self.count = count
    }

    func increment() {
        count += 1
    }
}
```

まずは、記事の冒頭で挙げた警告が出る理由を理解していきます。`init` に処理を書き加えると再現ができます。

```swift
actor Counter {
    var count: Int

    init(count: Int) {
        self.count = count

        print(self.count)
        print(self)
        print(self.count) // ❗️Cannot access property 'count' here in non-isolated initializer; this is an error in Swift 6
    }

    // ...
}
```

２回目の `print(self.count)` に対してのみ警告が出ていますが、これは sync な `init` の中で `self` に対して特定の操作をするとそこで `self` が non-isolated に変化するという性質のためです。 [SE-0327](https://github.com/apple/swift-evolution/blob/main/proposals/0327-actor-initializers.md) に従ってこの変化のことを decay と呼びます。
例えば関数の引数に `self` を渡すと decay が発生するので、それ以降は `self` は non-isolated になり、 sync な `init` からはアクセスできなくなるので上記の警告が出るというわけです。

このように関数の途中でいきなり `self` の性質が変わるのは不思議に思えるので、 decay がなぜ必要なのか考えていきます。
前提となる注意点として、actor の `init` は actor のコンテキストではなく呼び出し元のスレッドで実行されます。例えば、以下のように `@MainActor` である `UIViewController` の `viewDidLoad` から `Counter.init` を呼ぶと、 `init` の中身はメインスレッドで実行されます。

```swift
final class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let counter = Counter(count: 0) // メインスレッドで実行される
    }
}
```

これは、同期的に実行されるsync な `init` ではコンテキストを切り替えたくてもそのタイミングがないことを考えると自然です。

以上を念頭に置いた上で、例えば以下のようなコードを書くとデータ競合が起き得ることがわかります。

```swift
func incrementCounter(_ counter: Counter) {
    Task.detached {
        await counter.increment()
    }
}

actor Counter {
    var count: Int
    
    init(count: Int) {
        self.count = count

		// 以下２つの呼び出しでデータ競合が発生し得る
        incrementCounter(self) // 1
        self.increment()       // 2
    }
    
    func increment() {
        count += 1
    }
}
```

`1` の `incrementCounter` を介して呼び出される `increment` は外部から `await` で呼ばれているので actor のコンテキストで実行されるのに対して、 `2` で直接呼び出される `increment` は呼び出し元のスレッドで実行されます。これにより同時に２つのスレッドが `increment` を呼ぶ可能性があり、ひいては `count` に同時に書き込んでデータ競合が起き得ます。
これは `self` を別の関数に渡すことにより `init` と並行して何らかの処理が `self` に対して実行されるかもしれないことに起因する問題なので、 `self` を別の関数に渡して以降は `init` の中で直接 `self` が触れないように制限すればデータ競合は起きなくなります。そのために、別の関数に渡した以降は `self` が non-isolated になるように実装されており、これが decay です。実際に上記のコードを Xcode 14 で書くと decay により `2` の箇所で `Actor-isolated instance method 'increment()' can not be referenced from a non-isolated context; this is an error in Swift 6` という警告が出るようになり、データ競合の可能性にコンパイル時に気づけるようになっています。

注意点として、decay が発生しても `let` で宣言された `Sendable` なプロパティにはアクセスすることができます。これは、 `Sendable` な `let` は同時に複数のスレッドから触られても安全なためです。逆に、 `Sendable` でないプロパティや `Sendable` であっても `var` で定義されているプロパティについてはデータ競合が起き得るため、 decay により `self` が non-isolated になった状態でアクセスしようとすると警告が出ます。

```swift
actor MyActor {
    var nonSendableVar = NonSendableType()
    let nonSendableLet = NonSendableType()
    var sendableVar = SendableType()      
    let sendableLet = SendableType()

    
    init() {
        print(self) // decay によりこれ以降 self は non-isolated
        
        print(nonSendableVar) // ❗️Cannot access property 'nonSendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(nonSendableLet) // ❗️Cannot access property 'nonSendableLet' here in non-isolated initializer; this is an error in Swift 6
        print(sendableVar)    // ❗️Cannot access property 'sendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(sendableLet)    // ✅ OK
    }
}
```

ここまでは decay の原因として `self` を別の関数の引数に渡すことのみを挙げてきましたが、実際には以下の操作でも decay が起こります。これは、 `self` を別の関数の引数に渡すのと同じような原因により decay しないとデータ競合が発生し得るためです。

- `self` のメソッドを呼び出す
- `self` の compueted propery にアクセスする
- `self` のプロパティの `didSet` や `willSet` などのオブザーバを発火させる
- `self` をクロージャがキャプチャする
- `self` をメモリに保存する 

もちろん、上記の操作によって必ずしもデータ競合が発生し得る状況になるわけではありません。例えば、以下のコードではプロパティに対して値の読み込みしか行っていないのでデータ競合は発生しませんが、それでも decay は発生し警告が出ます。
```swift
actor Counter {
    var count: Int
    
    init(count: Int) {
        self.count = count
        print(self)
        print(self.count) // ❗️Cannot access property 'count' here in non-isolated initializer; this is an error in Swift 6
    }
}
```

これは、個々のケースについてコンパイラが `self` の使われ方を見てデータ競合が発生するかを判断するのがコストが高かったり、場合によっては不可能なために条件を満たしたら一括で decay するようになっているためです。

また、

- global actor に isolated された `init`
- `nonisolated` な `init`

でも sync な `init` と同じ条件を満たすと decay が発生します。これらはいずれも actor のコンテキスト外で実行されるという共通点を持ち、同様の原因でデータ競合を引き起こすためです。

## async な init におけるコンテキスト切り替え

async な `init` は sync な `init` とは別の振る舞いをします。sync な `init` で警告が出たコードをそのまま async な `init` に変更すると警告が出なくなります。

```swift
actor Counter {
    var count: Int

    init(count: Int) async {
        self.count = count

        print(self.count)
        print(self)
        print(self.count) // ✅ OK
    }
}
```

これは、 async な `init` ではすべてのプロパティが初期化された時点で暗黙のうちに actor のコンテキストに切り替わって実行されるためです。上記の `print(self.count)` は sync な `init` とは異なり actor のコンテキスト内で実行されているので、同期的にアクセスしても何も問題ないというわけです。

コンテキストの切り替えが実際に発生していることを確認するため、複数のプロパティを持つ actor の `init` をメインスレッドから実行してみます。

```swift
actor Point {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) async {
        print(Thread.current.description) // <_NSMainThread: 0x600001704080>{number = 1, name = main}
        self.x = x
        print(Thread.current.description) // <_NSMainThread: 0x600001704080>{number = 1, name = main}
        self.y = y 
        // ここですべてのプロパティが初期化されたのでスレッドが切り替わる

        print(Thread.current.description) // <NSThread: 0x600001700240>{number = 2, name = (null)}
    }
}
```

`x` と `y` が初期化されるまではそのままメインスレッドで実行されていますが、初期化されて以降は actor のコンテキストで実行されるためにスレッドが切り替わっていることがわかります。この振る舞いにより、例えば以下の `1` と `2` どちらから呼ばれる `increment` も actor のコンテキスト上で実行されることからデータ競合が起こることはありません。

```swift
func incrementCounter(_ counter: Counter) {
    Task.detached {
        await counter.increment()
    }
}

actor Counter {
    var count: Int
    
    init(count: Int) async {
        self.count = count

        incrementCounter(self) // 1
        self.increment()       // 2
    }
    
    func increment() {
        count += 1
    }
}
```

つまり、sync な `init` ではデータ競合を decay により回避しているのに対し、 async な `init` では自動的に actor のコンテキストに切り替えることで回避しているということです。これは、async な `init` では suspend してコンテキストを切り替えることが可能であるために取れる方法です。

### コンテキスト切り替えが暗黙的に行われる理由

コンテキスト切り替えは通常 `await` を挟んで開発者が明確に認識できる形で発生するので、 `actor` の async `init` ではこれが暗黙のうちに行われるのは特徴的です。 [SE-0327](https://github.com/apple/swift-evolution/blob/main/proposals/0327-actor-initializers.md) ではその理由として以下が挙げられています。

- すべてのプロパティが初期化されるタイミングは、 `init` の引数が増えたりデフォルト引数が設定されたりなどの変更によって変わってしまうので、もしコンテキスト切り替えのタイミングを `await` などでマークしないといけない場合はそのたびにコードの書き換えが発生してわずらわしい
- そもそもプロパティ初期化でコンテキストが切り替わるというのは実装の詳細であって、開発者が意識する必要はない
- actor の処理の中で `await` が挟まる場合は actor reentrancy が発生し得るので注意する必要があり、それが明示的に `await` を書く理由にもなっているが、 `init` の場合はプロパティの初期化が行われるまでは `init` 以外が actor インスタンスにアクセスすることができないので reentrancy が発生し得ない

actor reentrancy について、よければ以下の記事も参照してみてください。

https://qiita.com/maiyama18/items/c815f13f17d8e4095f68

### プロパティが初期化されるまでの `self` の弱い isolation

ここまで考えると逆にすべてのプロパティが初期化されるまでは actor のコンテキスト外で実行されているのに同期的にプロパティを設定できるのが不思議に思えてきます。

```swift
actor Point {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) async {
        self.x = x // actor のコンテキスト外なのに同期的に値を設定できている
        self.y = y
    }
}
```

これは、すべてのプロパティを初期化するまでは `init` 以外から `self` を触ることができないため、実質的に `self` が isolated されているためです。 [SE-0327](https://github.com/apple/swift-evolution/blob/main/proposals/0327-actor-initializers.md)  ではこのことを

> weaker form of isolation that relies on having exclusive access to the reference `self` 

と表現しています。実際、すべてのプロパティの初期化前に `self` にアクセスしようとすると以下のようにエラーになります。
```swift
actor Point {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) async {
        self.x = x
        doSomething(self) // ❗️'self' used in method call 'doSomething' before all stored properties are initialized
                          // self.y が未設定なので init 以外からは `self` を使えない
        self.y = y
    }
}
```

`init` しか `self` に触ることができないならば当然データ競合の心配もないため、文法上 actor のプロパティをコンテキスト外から同期的に設定できるということです。逆に、これが許されていない場合は actor のインスタンスをコンテキスト外から初期化できなくなってしまいます。

sync な `init` で decay が発生するまでは同期的にプロパティが設定できるのも同じ理由です。

## deinit における decay 

ここまで sync / async それぞれの `init` について書きましたが、 actor の `deinit` にも注意すべき点があるので見ていきます。

### deinit における decay

`deinit` は参照型の reference count が０になった時に任意のスレッドから呼ばれます。これは actor であっても同じなので、actor の `deinit` は actor のコンテキスト外で実行されるということになります。
コンテキスト外で実行されるということは sync な `init` と同様にデータ競合が起こり得るということなので `deinit` でもこれを回避するための decay が発生します。decay の条件も `init` と同じで、以下の例では `self` を `print` 関数の引数に渡すことで、それ以降は `self` が non-isolated になっています。

```swift
actor Counter {
    var count: Int
    
    deinit {
        print(self.count)
        print(self)
        print(self.count) // ❗️Cannot access property 'count' here in deinitializer; this is an error in Swift 6
    }
}
```

逆に、 `self` を関数の引数として渡すなどして decay の条件を満たすまでは `self` にアクセスできるのは `deinit` を呼んだスレッドだけなので `self` は isolated されていて安全に同期的な操作をすることができます。

### deinit における `Sendable` でないプロパティへのアクセス制限

decay に加えて、 `deinit` では decay が発生する前であっても `Sendable` でないプロパティにはアクセスできないという制限が加わります。
```swift
actor MyActor {
    var nonSendableVar = NonSendableType()
    let nonSendableLet = NonSendableType()
    var sendableVar = SendableType()
    let sendableLet = SendableType()
    
    init() {
        print(nonSendableVar) // ✅ OK
        print(nonSendableLet) // ✅ OK
        print(sendableVar)
        print(sendableLet)
        
        print(self)
        
        print(nonSendableVar) // ❗️Cannot access property 'nonSendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(nonSendableLet) // ❗️Cannot access property 'nonSendableLet' here in non-isolated initializer; this is an error in Swift 6
        print(sendableVar)    // ❗️Cannot access property 'sendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(sendableLet)
    }
    
    deinit {
        // init と異なり decay 前の non-Sendable なプロパティへのアクセスもできないようになっている
        print(nonSendableVar) // ❗️Cannot access property 'nonSendableVar' with a non-sendable type 'NonSendableType' from non-isolated deinit; this is an error in Swift 6
        print(nonSendableLet) // ❗️Cannot access property 'nonSendableLet' with a non-sendable type 'NonSendableType' from non-isolated deinit; this is an error in Swift 6
        print(sendableVar)
        print(sendableLet)
        
        print(self)
        
        print(nonSendableVar) // ❗️Cannot access property 'nonSendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(nonSendableLet) // ❗️Cannot access property 'nonSendableLet' here in non-isolated initializer; this is an error in Swift 6
        print(sendableVar)    // ❗️Cannot access property 'sendableVar' here in non-isolated initializer; this is an error in Swift 6
        print(sendableLet)
    }
}

```

前提として、 global actor はすべてのインスタンスの処理が１つの executor で行われるため複数のインスタンスの間で `Sendable` でないプロパティを安全に共有することができます。 `deinit` は actor のコンテキスト外で実行されるにも関わらず decay の前はプロパティに同期的にアクセスできてしまうので、複数のスレッドからその `Sendable` でないプロパティにアクセスを行うことでデータ競合が発生する可能性があります。言い換えると、global actor で `Sendable` でないプロパティが共有できるのは、同じ global actor に従う複数のインスタンスの処理が同時に実行されないという前提があるからですが、 `deinit` ではこれが崩れるということです。

例えば以下のような実装により `deinit` でデータ競合が発生することがあります。

```swift
class NonSendableCounter {
    var count: Int = 0
    func increment() { count += 1 }
}

@MainActor
final class Container {
    let counter: NonSendableCounter
    
    init() {
        self.counter = NonSendableCounter()
    }
    
    init(sharingCounterWith other: Container) {
        // self と other はいずれも MainActor であるため同時に処理が実行されることはなく、
        // Sendable なプロパティであっても（deinit のことを考えなければ）安全に共有できる
        self.counter = other.counter
    }
    
    deinit {
		// deinit は actor のコンテキスト外で実行されるので self と other の deinit が同時に実行され得る
		// 同時に実行された場合、データ競合が発生する可能性がある
        counter.increment()
    }
}
```

人工的なコードですが、上記の `Container` を以下のように操作することで実際にデータ競合が発生することが確認できました。

```swift
var container1: Container? = .init()
var container2: Container? = .init(sharingCounterWith: container1!)
let container3: Container = .init(sharingCounterWith: container1!)

// counter1 と counter2 の deinit が同時に実行される可能性がある
let t1 = Task.detached { container1 = nil }
let t2 = Task.detached { container2 = nil }

try await (t1.value, t2.value)

print(container3.counter.count) // 1 or 2
```

`deinit` の中で `Sendable` でないプロパティが触れない制限を回避するために `Task` を起動する方法を思いつくかもしれませんが、これは避けた方がいいでしょう。

```swift
actor MyActor {
    var nonSendableVar = NonSendableType()
    
    deinit {
        Task.detached {
            await print(self.nonSendableVar)
        }
    }
}
```

`deinit` は reference count が０になった時に呼ばれますが、このように `deinit` の中でさらに `self` の参照を増やすと reference count が１に増えてしまい、再度０になった時の動作は未定義なのでクラッシュが発生する可能性があるようです。

## 参考
- [SE-0327 On Actors and Initialization](https://github.com/apple/swift-evolution/blob/main/proposals/0327-actor-initializers.md)
- [LFK Dev mobile devpods - Swift actorのinitとdeinitの新しいルールを理解する](https://lfk-devpods.linecorp.com/episode/16)