34
11

【TCA】Sendableと@SendableとTCA

Last updated at Posted at 2023-05-13

はじめに

TCAを使ってアプリを実装しているときによく出くわすあの書き方は、Sendableを理解することでよく理解できるようになります。
本記事ではまずSendableについて解説し、その後TCAでSendableに出くわすポイントについて解説してみたいと思います。

※説明不足、間違い等々ありましたら、是非コメントください

Sendableとは?

準拠する型がデータ競合が発生せず安全に渡せるデータであることを表すものです。
Swiftのドキュメントでは、ある同時実行ドメインから別の同時実行ドメインに共有できる型と表現されています。

データ競合(Data Races)とは?

複数のスレッドが同時に同じデータにアクセスし、かつそのうちの少なくとも一つのスレッドが書き込み処理を行うことにより、データが不整合な状態になる現象のことです。

データ競合を発生させてみる

以下のコードを実行すると、出力結果は1000にはなりません。
そして実行するたびに結果が異なります。

class Counter {
    var count = 0

    func increment() {
        count += 1
    }
}

let counter = Counter()

for _ in 0..<1000 {
    Task {
        counter.increment()
    }
}

Thread.sleep(forTimeInterval: 2)
print(counter.count)

Sendableの必要性とは?

上記コードにおけるCounterクラスはデータ競合が発生するので、同時実行ドメイン間で安全に共有することができません。
にもかかわらず、コンパイルは問題なく通ってしまいます。

Sendableはこれを解決するために導入されました。
Sendableはデータ競合を発生させる可能性のあるコードをコンパイル時に検出することを可能にします。

問題を検出できるようにする

我々にはSendable対応のための猶予が与えられているので、現状では上記のようなコードを書いても警告も出ないしエラーにもなりません。
しかし、Swift6からはエラーになってコンパイルできなくなります。

いますぐに問題を検出できるようにするには、Swiftコンパイラにフラグを渡してあげます。

swiftSettings: [
    .unsafeFlags(["-Xfrontend", "-warn-concurrency"])
]

こうすると、該当箇所に警告が表示されるようになります。

Untitled.png

Capture of 'counter' with non-sendable type 'Counter' in a @Sendable closure

Taskのクロージャは@Sendable

Task.initのシグネチャはこうなっていて、クロージャが@Sendableになっていることがわかります。

public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success)

クロージャが@Sendableとはどういうことか?

関数またはクロージャに@Sendableをつけると、以下が適用されます。

  • キャプチャする型はSendableへの準拠が必要
  • 変数はキャプチャできない

前述のCounterクラスは非Sendableなため、1つ目の要件に合致せず警告が出ていたことがわかります。

Sendableな型であっても、クロージャ内で変数をキャプチャしている以下のコードはコンパイルエラーになります。
これについては-warn-concurrencyフラグをつけていなくてもエラーになります。

var count = 0
Task {
    print(count)  // Reference to captured var 'count' in concurrently-executing code
}

キャプチャリストを使用することで、変数の値を利用することができます。
以下のクロージャ内のcountはクロージャの外にあるcountとは別の新しい不変変数だからです。

var count = 0
Task { [count] in
    print(count)
}

TCAでSendableを意識するポイント

ここまで理解すると、TCAのあの書き方の意味がわかります。

EffectTask.task / run / fireAndForgetに渡すクロージャも@Sendable

.taskに渡すクロージャは@Sendableであり、stateはinoutで渡されるために可変であるため、クロージャ内でキャプチャすることができません。

そこで、stateの値をキャプチャリストでコピーし、クロージャ内で参照可能にしています。

func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
    switch action {
    case .onAppear:
        return .task { [state] in
            await fetchResponse(TaskResult {
                try await repository.fetch(.init(newsId: state.newsId))
            })
        }
    }
}

依存のプロパティを@Sendableにしている理由

TCAで推奨されている依存(XxxClientとかXxxRepositoryとか)のインタフェース(プロパティ)はクロージャになっていて、@Sendableをつけています。

public struct XxxClient {
    public var fetch: @Sendable () async throws -> Entity
}

依存はEffectTask.taskなど並行コンテキストで利用されるため、データ競合を引き起こす可能性があります。

例えば、カウント値を保持し、increment()を呼ぶごとにカウントアップした値を返す機能を持つClientを考えます。

struct CountClient {
    var increment: () -> Int
}

このClientを以下のように実装するとしましょう。

extension CountClient {
    static func live() -> Self {
        var count = 0

        return .init(increment: {
            count += 1
            return count
        })
    }
}

Counterクラスの例と同様に以下のような処理を実装すると、データ競合が発生してしまいます。
最終的な出力は1001を期待したいですが、実際はそうなりません。

let client = CountClient.live()

for _ in 0..<1000 {
    Task {
        _ = client.increment()
    }
}

Thread.sleep(forTimeInterval: 2)
print(client.increment())

インタフェースに@Sendableをつけることで、データ競合が発生するコードを実装できないようにすることができます(-warn-concurrencyをつけている場合)。

具体的には、以下のようにコンパイルエラーになります。

struct Repository {
    var increment: @Sendable () -> Int
}

extension Repository {
    static func live() -> Self {
        var count = 0

        return .init(increment: {
            count += 1    // Mutation of captured var 'count' in concurrently-executing code
            return count  // Reference to captured var 'count' in concurrently-executing code
        })
    }
}

TCAのドキュメントで以下のように言及されています。

This will restrict the kinds of closures that can be used when constructing FactClient values, thus making the entire FactClient sendable itself.

インタフェースを@Sendableにすると表現しましたが、DependencyValuesに登録するためにはClient自体をSendableにする必要があります。

参考

34
11
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
11