概要
Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter; later accesses could race
というエラーが発生して、その原因と解決策を調べたので備忘録として残しておきます。SwiftのConcurrency(非同期処理)に関連したエラーで、タスク分離やSendableプロトコルに関する理解を深めるための例として役立つと思います。
発生した時のコードを簡略化したのものが以下になります。
final class ViewModel {
func fetchData() async {
print("Fetching data...")
}
func runTask() {
Task {
await fetchData()
}
}
}
原因
このエラーの原因を一言でいうと、「Sendable
じゃない関数が異なるTask間で渡されているから」です。
Sendableとは
Sendable
は 「値が複数のスレッドから安全にアクセスできること」 を保証するプロトコルです。もしSendableに準拠してない値や関数が別々のスレッドから参照されると、データ競合や安全性の欠如につながる可能性があります。そのため異なるTask間で値や関数を参照する際には、Sendableに準拠していなければいけないというルールがあります。
エラーの詳細
上記のコードでは、runTaskでTaskを新しく作ってfetchDataを呼び出してますが、fetchDataはSendableではないのでこのエラーが発生していることになります。
- runTaskメソッドで新しいTaskを作成
- 新しいTask内で、fetchDataメソッドを呼び出す
- fetchDataがSendableに準拠していないため、「タスク間での安全な転送が保証されない」としてエラーが発生する
簡単に図にするとのこんな感じです。
解決策
このエラーを解決するためのアプローチを3つ紹介します。それぞれの方法にはメリットがあり、実際の要件やコード状況に応じて選択してください。
クラスにSendableを準拠させる
1つ目の解決策は、クラスにSendable
を準拠させるものです。
final class ViewModel: Sendable {
func fetchData() async {
print("Fetching data...")
}
func runTask() {
Task { [weak self] in
guard let self = self else { return }
await self.fetchData()
}
}
}
ポイント
- クラスがSendableに準拠させると、そのインスタンスのメソッドがスレッドセーフであることが保証されるのでエラーが解消される
新しいTaskを作らない
2つ目の解決策は、そもそも新しいTaskを生成しないことです。
final class ViewModel {
func fetchData() async {
print("Fetching data...")
}
func runTask() async {
await self.fetchData()
}
}
ポイント
- runTask自体を非同期メソッドに変更
- 新しいTaskを生成しないため、タスク間での安全性の問題がそもそも発生しない
- シンプルな解決策
参照するメソッドをstaticにする
3つ目の解決策は、参照するメソッドをstaticにすることです。
final class ViewModel {
static func fetchData() async {
print("Fetching data...")
}
func runTask() {
Task {
await ViewModel.fetchData()
}
}
}
ポイント
- staticメソッドはインスタンスの状態に依存しないため、スレッドセーフに扱える