クソ分かりやすい
Swift 並行処理

async/await、Tasks、そしてコンパイラがなぜあなたに怒鳴り続けるのかを、ついに理解しよう。

Matt Massicotte 氏に多大な感謝を。Swift 並行処理を理解可能にしてくれました。Tuist 共同創設者の Pedro Piñera がまとめました。問題を見つけた? Issue を開くPR を送る

非同期コード: async/await

アプリがやることの大半は待つことだ。サーバーからデータを取得する - レスポンスを待つ。ディスクからファイルを読む - バイトを待つ。データベースにクエリする - 結果を待つ。

Swift の並行処理システム以前は、この待機をコールバック、デリゲート、または Combine で表現していた。動くけど、ネストしたコールバックは追いづらくなるし、Combine は学習曲線がきつい。

async/await は Swift に待機を処理する新しい方法を与える。コールバックの代わりに、シーケンシャルに見えるコードを書く - 一時停止し、待ち、再開する。内部では、Swift のランタイムがこれらの一時停止を効率的に管理する。ただし、待っている間にアプリを実際にレスポンシブに保つかどうかは、コードがどこで実行されるかに依存する - これは後で説明する。

async 関数は一時停止が必要になるかもしれない関数だ。async でマークし、呼び出すときは await を使って「これが終わるまでここで一時停止」と言う:

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)  // ここで中断
    return try JSONDecoder().decode(User.self, from: data)
}

// 呼び出し
let user = try await fetchUser(id: 123)
// ここのコードは fetchUser が完了した後に実行される

await でコードは一時停止する - これを中断と呼ぶ。作業が終わると、コードは中断した場所から正確に再開する。中断は Swift に待っている間に他の作業をする機会を与える。

彼らを待つ

複数のものを取得する必要がある場合は?一つずつ await できる:

let avatar = try await fetchImage("avatar.jpg")
let banner = try await fetchImage("banner.jpg")
let bio = try await fetchBio()

でもこれは遅い - 各々が前のものが終わるのを待つ。async let を使って並列に実行しよう:

func loadProfile() async throws -> Profile {
    async let avatar = fetchImage("avatar.jpg")
    async let banner = fetchImage("banner.jpg")
    async let bio = fetchBio()

    // 3つすべてが並列で取得中!
    return Profile(
        avatar: try await avatar,
        banner: try await banner,
        bio: try await bio
    )
}

async let はすぐに開始する。await が結果を収集する。

await には async が必要

awaitasync 関数の中でしか使えない。

作業の管理: Tasks

Task は管理できる非同期作業の単位だ。async 関数を書いてきたけど、Task がそれを実際に実行するものだ。同期コードから非同期コードを開始する方法であり、その作業を制御できる: 結果を待つ、キャンセルする、バックグラウンドで実行させる。

プロフィール画面を作っているとしよう。ビューが表示されたときにアバターを読み込むには、.task モディファイアを使う。ビューが消えると自動的にキャンセルされる:

struct ProfileView: View {
    @State private var avatar: Image?

    var body: some View {
        Group {
            if let avatar {
                avatar
            } else {
                ProgressView()
            }
        }
        .task { avatar = await downloadAvatar() }
    }
}

ユーザーがプロフィール間を切り替えられる場合は、.task(id:) を使って選択が変わったときにリロードする:

struct ProfileView: View {
    var userID: String
    @State private var avatar: Image?

    var body: some View {
        Group {
            if let avatar {
                avatar
            } else {
                ProgressView()
            }
        }
        .task(id: userID) { avatar = await downloadAvatar(for: userID) }
    }
}

ユーザーが「保存」をタップしたら、Task を手動で作成する:

Button("Save") {
    Task { await saveProfile() }
}

Taskの結果にアクセスする

Taskを作成すると、ハンドルが返される。.valueを使って結果を待って取得する:

let handle = Task {
    return await fetchUserData()
}
let userData = await handle.value  // タスクが完了するまで中断

これは後で結果が必要な場合や、タスクハンドルを保存して別の場所でawaitしたい場合に便利。

アバター、自己紹介、統計情報を一度に読み込む必要がある場合は?TaskGroup を使って並列に取得する:

try await withThrowingTaskGroup(of: Void.self) { group in
    group.addTask { avatar = try await downloadAvatar(for: userID) }
    group.addTask { bio = try await fetchBio(for: userID) }
    group.addTask { stats = try await fetchStats(for: userID) }
    try await group.waitForAll()
}

グループ内の Task は子タスクであり、親にリンクされている。知っておくべきこと:

  • キャンセルは伝播する: 親をキャンセルすると、すべての子もキャンセルされる
  • エラー: スローされたエラーは兄弟をキャンセルして再スローするが、next()waitForAll()、またはイテレーションで結果を消費したときのみ
  • 完了順: 結果は追加した順ではなく、タスクが完了した順に届く
  • すべてを待つ: すべての子が完了するかキャンセルされるまでグループは戻らない

これが**構造化並行処理**だ: 推論しやすくクリーンアップしやすいツリー構造で組織された作業。

どこで実行されるか: スレッドから分離ドメインへ

ここまでコードがいつ実行されるか(async/await)とどう組織するか(Tasks)について話してきた。次は: どこで実行され、どうやって安全を保つか?

ほとんどのアプリはただ待っている

ほとんどのアプリコードは I/O バウンドだ。ネットワークからデータを取得し、レスポンスを await し、デコードして、表示する。複数の I/O 操作を調整する必要があれば、taskstask groups に頼る。実際の CPU 作業は最小限だ。メインスレッドはこれを問題なく処理できる。なぜなら await はブロックせずに中断するから。

でも遅かれ早かれ、CPU バウンドな作業に出会う: 巨大な JSON ファイルのパース、画像処理、複雑な計算の実行。この作業は外部の何かを待たない。ただ CPU サイクルが必要なだけだ。メインスレッドで実行すると、UI がフリーズする。ここで「コードがどこで実行されるか」が本当に重要になる。

旧世界: 多くの選択肢、安全性なし

Swift の並行処理システム以前は、実行を管理するいくつかの方法があった:

アプローチ 何をするか トレードオフ
Thread 直接スレッド制御 低レベル、エラーが起きやすい、ほとんど不要
GCD クロージャ付きディスパッチキュー シンプルだがキャンセルなし、スレッド爆発を起こしやすい
OperationQueue タスク依存関係、キャンセル、KVO より制御できるが冗長で重い
Combine リアクティブストリーム イベントストリームに最適、学習曲線がきつい

これらはすべて動いたが、安全性は完全に自分次第だった。メインへのディスパッチを忘れたり、二つのキューが同じデータに同時にアクセスしても、コンパイラは助けてくれなかった。

問題: データレース

データレースは、二つのスレッドが同じメモリに同時にアクセスし、少なくとも一方が書き込んでいるときに起こる:

var count = 0

DispatchQueue.global().async { count += 1 }
DispatchQueue.global().async { count += 1 }

// 未定義動作: クラッシュ、メモリ破損、または間違った値

データレースは未定義動作だ。クラッシュしたり、メモリを破損したり、静かに間違った結果を生成したりする。テストではアプリが問題なく動き、本番環境でランダムにクラッシュする。ロックやセマフォなどの従来のツールは助けになるが、手動でエラーが起きやすい。

並行処理は問題を増幅する

アプリの並行度が高いほど、データレースの可能性が高くなる。シンプルな iOS アプリはずさんなスレッドセーフティでも何とかなるかもしれない。何千もの同時リクエストを処理する Web サーバーは常にクラッシュする。これが Swift のコンパイル時安全性が高並行環境で最も重要な理由だ。

シフト: スレッドから分離へ

Swift の並行処理モデルは異なる質問をする。「どのスレッドで実行すべきか?」ではなく、**「誰がこのデータにアクセスすることを許可されているか?」**と尋ねる。

これが分離だ。手動で作業をスレッドにディスパッチする代わりに、データの周りに境界を宣言する。コンパイラがこれらの境界をランタイムではなくビルド時に強制する。

内部の仕組み

Swift 並行処理は libdispatch(GCD と同じランタイム)の上に構築されている。違いはコンパイル時レイヤーだ: アクターと分離はコンパイラによって強制され、ランタイムは CPU のコア数に制限された協調スレッドプールでスケジューリングを処理する。

三つの分離ドメイン

1. MainActor

@MainActor はメインスレッドの分離ドメインを表すグローバルアクターだ。UI フレームワーク(UIKit、AppKit、SwiftUI)がメインスレッドアクセスを必要とするため、特別だ。

@MainActor
class ViewModel {
    var items: [Item] = []  // MainActor 分離で保護される
}

何かを @MainActor でマークするとき、「これをメインスレッドにディスパッチする」とは言っていない。「これはメインアクターの分離ドメインに属する」と言っている。コンパイラは、これにアクセスするものは MainActor 上にいるか、境界を越えるために await しなければならないことを強制する。

迷ったら @MainActor を使え

ほとんどのアプリでは、ViewModel に @MainActor をマークするのが正しい選択だ。パフォーマンスの懸念は通常大げさだ。ここから始めて、実際に問題を測定した場合のみ最適化しよう。

2. Actors

actor は自身の可変状態を保護する。一度に一つのコードだけがそのデータにアクセスできることを保証する:

actor BankAccount {
    var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount  // 安全: アクターが排他的アクセスを保証
    }
}

// 外部からは、境界を越えるために await しなければならない
await account.deposit(100)

アクターはスレッドではない。 アクターは分離境界だ。Swift ランタイムがどのスレッドが実際にアクターコードを実行するかを決定する。それを制御することはできないし、する必要もない。

3. Nonisolated

nonisolated でマークされたコードはアクター分離をオプトアウトする。await なしでどこからでも呼び出せるが、アクターの保護された状態にはアクセスできない:

actor BankAccount {
    var balance: Double = 0

    nonisolated func bankName() -> String {
        "Acme Bank"  // アクター状態にアクセスしない、どこからでも呼び出し安全
    }
}

let name = account.bankName()  // await 不要

親しみやすい並行処理: 摩擦を減らす

親しみやすい並行処理は2つの Xcode ビルド設定でメンタルモデルをシンプルにする:

  • SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor:他に指定しない限りすべてが MainActor で実行される
  • SWIFT_APPROACHABLE_CONCURRENCY = YESnonisolated async 関数はバックグラウンドスレッドにジャンプする代わりに呼び出し元のアクターにとどまる

新しい Xcode 26 プロジェクトではデフォルトで両方が有効になっている。メインスレッドから外れた CPU 集約的な作業が必要なときは @concurrent を使う。

// MainActor で実行(デフォルト)
func updateUI() async { }

// バックグラウンドスレッドで実行(オプトイン)
@concurrent func processLargeFile() async { }

オフィスビル

アプリをオフィスビルと考えよう。各分離ドメインはドアにロックがかかったプライベートオフィスだ。一度に一人だけが中に入って、そのオフィスの書類を扱える。

  • MainActor は受付 - すべての顧客対応が行われる場所。一つしかなく、ユーザーが見るすべてを処理する。
  • actor 型は部門オフィス - 経理、法務、人事。それぞれが自分の機密書類を保護する。
  • nonisolated コードは廊下 - 誰でも歩ける共有スペースだが、プライベートな書類はそこにはない。

他人のオフィスに無断で入ることはできない。ノックして(await)、入れてもらうのを待つ。

分離ドメインを越えられるもの: Sendable

分離ドメインはデータを保護するが、最終的にはドメイン間でデータを渡す必要がある。そうするとき、Swift は安全かどうかをチェックする。

考えてみよう: 可変クラスへの参照をあるアクターから別のアクターに渡すと、両方のアクターが同時にそれを変更できてしまう。まさに防ごうとしているデータレースだ。だから Swift は知る必要がある: このデータは安全に共有できるか?

答えは Sendable プロトコルだ。コンパイラに「この型は分離境界を越えて渡しても安全」と伝えるマーカーだ:

  • Sendable 型は安全に越えられる(値型、不変データ、アクター)
  • Non-Sendable 型は越えられない(可変状態を持つクラス)
// Sendable - 値型なので、各場所がコピーを得る
struct User: Sendable {
    let id: Int
    let name: String
}

// Non-Sendable - 可変状態を持つクラス
class Counter {
    var count = 0  // 二箇所がこれを変更 = 災害
}

型を Sendable にする

Swift は多くの型で Sendable を自動的に推論する:

  • Sendable プロパティのみを持つ構造体と列挙型は暗黙的に Sendable
  • アクターは常に Sendable - 自身の状態を保護するから
  • @MainActorSendable - MainActor がアクセスを直列化するから

クラスの場合は難しい。クラスが Sendable に準拠できるのは、final で、すべての格納プロパティが不変の場合のみ:

final class APIConfig: Sendable {
    let baseURL: URL      // 不変
    let timeout: Double   // 不変
}

他の手段(ロック、アトミック)でスレッドセーフなクラスがある場合は、@unchecked Sendable を使ってコンパイラに「信じて」と伝えられる:

final class ThreadSafeCache: @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: Data] = [:]
}

@unchecked Sendable は約束だ

コンパイラはスレッドセーフティを検証しない。間違っていればデータレースになる。控えめに使おう。

親しみやすい並行処理: 摩擦を減らす

親しみやすい並行処理では、Sendable エラーはずっと少なくなる:

  • コードが分離境界を越えないなら、Sendable は不要
  • async 関数はバックグラウンドスレッドにホップする代わりに呼び出し元のアクターにとどまる
  • コンパイラは値が安全に使われているかの検出が賢くなる

SWIFT_DEFAULT_ACTOR_ISOLATIONMainActor に、SWIFT_APPROACHABLE_CONCURRENCYYES に設定して有効にする。新しい Xcode 26 プロジェクトではデフォルトで両方が有効になっている。並列性が本当に必要なときは、関数を @concurrent でマークしてから Sendable について考えよう。

コピーと原本

オフィスビルに戻ろう。部門間で情報を共有する必要があるとき:

  • コピーは安全 - 法務部が書類のコピーを作って経理部に送れば、両方が自分のコピーを持つ。好きなように落書きしたり変更したりできる。衝突なし。
  • 署名入りの原本契約はその場にとどまるべき - 二つの部門が両方とも原本を変更できたら、カオスになる。どれが本物のバージョン?

Sendable 型はコピーのようなもの: 各場所が独立したコピーを得る(値型)か、不変である(誰も変更できない)から共有しても安全。Non-Sendable 型は原本契約のようなもの: 渡し回すと矛盾する変更の可能性が生まれる。

分離がどう継承されるか

分離ドメインがデータを保護し、Sendable がその間を越えるものを制御することを見てきた。でも、そもそもコードはどうやって分離ドメインに入るのか?

関数を呼び出したりクロージャを作成したりすると、分離はコードを通じて流れる。親しみやすい並行処理では、アプリは MainActor から始まり、何かが明示的に変更しない限り、その分離は呼び出すコードに伝播する。このフローを理解することで、コードがどこで実行されるか、なぜコンパイラが時々文句を言うかを予測できる。

関数呼び出し

関数を呼び出すと、その分離がどこで実行されるかを決定する:

@MainActor func updateUI() { }      // 常に MainActor で実行
func helper() { }                    // 呼び出し元の分離を継承
@concurrent func crunch() async { }  // 明示的にオフアクターで実行

親しみやすい並行処理では、ほとんどのコードが MainActor 分離を継承する。関数は呼び出し元がいる場所で実行される - 明示的にオプトアウトしない限り。

クロージャ

クロージャは定義されたコンテキストから分離を継承する:

@MainActor
class ViewModel {
    func setup() {
        let closure = {
            // ViewModel から MainActor を継承
            self.updateUI()  // 安全、同じ分離
        }
        closure()
    }
}

これが SwiftUI の Button アクションクロージャが安全に @State を更新できる理由だ: ビューから MainActor 分離を継承している。

Tasks

Task { } は作成された場所からアクター分離を継承する:

@MainActor
class ViewModel {
    func doWork() {
        Task {
            // MainActor 分離を継承
            self.updateUI()  // 安全、await 不要
        }
    }
}

これは通常望む動作だ。タスクはそれを作成したコードと同じアクターで実行される。

継承を断ち切る: Task.detached

コンテキストを何も継承しないタスクが欲しいこともある:

@MainActor
class ViewModel {
    func doHeavyWork() {
        Task.detached {
            // アクター分離なし、協調プールで実行
            let result = await self.expensiveCalculation()
            await MainActor.run {
                self.data = result  // 明示的に戻る
            }
        }
    }
}

Task と Task.detached はアンチパターン

Task { ... } でスケジュールするタスクは管理されない。キャンセルする方法も、いつ終わるか知る方法もない。戻り値にアクセスする方法も、エラーに遭遇したか知る方法もない。ほとんどの場合、.taskTaskGroup で管理されるタスクを使う方が良い。「よくある間違い」セクションで説明されている通り

Task.detached は最後の手段であるべき。デタッチされたタスクは優先度、タスクローカル値、アクターコンテキストを継承しない。メインアクターから外れた CPU 集約的な作業が必要なら、代わりに関数を @concurrent でマークしよう。

Async ユーティリティでの分離の保持

クロージャを受け取るジェネリックな async 関数を書くことがある - ラッパー、リトライヘルパー、トランザクションスコープ。呼び出し元がクロージャを渡し、関数がそれを実行する。シンプルだよね?

func measure<T>(
    _ label: String,
    block: () async throws -> T
) async rethrows -> T {
    let start = ContinuousClock.now
    let result = try await block()
    print("\(label): \(ContinuousClock.now - start)")
    return result
}

でも @MainActor コンテキストからこれを呼び出すと、Swift が文句を言う:

Sending value of non-Sendable type '() async throws -> T' risks causing data races

何が起きている?クロージャは MainActor の状態をキャプチャしているが、measurenonisolated だ。Swift は非 Sendable なクロージャが分離境界を越えようとしているのを見ている - まさに防ぐように設計されていることだ。

最もシンプルな修正は nonisolated(nonsending) だ。これは関数を呼び出し元のエクゼキュータに留まらせる:

nonisolated(nonsending)
func measure<T>(
    _ label: String,
    block: () async throws -> T
) async rethrows -> T {
    let start = ContinuousClock.now
    let result = try await block()
    print("\(label): \(ContinuousClock.now - start)")
    return result
}

これで関数全体が呼び出し元のエクゼキュータで実行される。MainActor から呼べば MainActor に留まる。カスタムアクターから呼べばそこに留まる。クロージャは分離境界を越えないので、Sendable チェックは不要だ。

どちらを使うべきか

nonisolated(nonsending) - シンプルな選択。属性を追加するだけ。呼び出し元のエクゼキュータに留まりたいだけならこれを使う。

isolation: isolated (any Actor)? = #isolation - 明示的な選択。アクターインスタンスにアクセスできるパラメータを追加する。分離コンテキストを他の関数に渡したり、どのアクターにいるか調べたりする必要があるときに使う。

明示的にアクターにアクセスする必要がある場合は、代わりに #isolation パラメータを使う:

func measure<T>(
    isolation: isolated (any Actor)? = #isolation,
    _ label: String,
    block: () async throws -> T
) async rethrows -> T {
    let start = ContinuousClock.now
    let result = try await block()
    print("\(label): \(ContinuousClock.now - start)")
    return result
}

どちらのアプローチも、自然に使える async ユーティリティを作るのに不可欠だ。これがないと、呼び出し元はクロージャを @Sendable にするか、コンパイラを満足させるために複雑なことをする必要がある。

ビルを歩く

受付オフィス(MainActor)にいて、手伝いを呼ぶと、彼らはあなたのオフィスに来る。あなたの場所を継承する。タスクを作成すると(「これをやっておいて」)、そのアシスタントもあなたのオフィスから始まる。

誰かが別のオフィスに行くのは、明示的にそこに行く場合だけだ:「このためには経理で作業する必要がある」(actor)、または「これは奥のオフィスで処理する」(@concurrent)。

すべてをまとめる

一歩引いて、すべてのピースがどうフィットするか見てみよう。

Swift 並行処理は多くの概念に感じられる: async/awaitTask、アクター、MainActorSendable、分離ドメイン。でも実際には中心にあるのは一つのアイデアだけだ: 分離はデフォルトで継承される

親しみやすい並行処理を有効にすると、アプリは MainActor から始まる。それが出発点だ。そこから:

  • 呼び出すすべての関数がその分離を継承する
  • 作成するすべてのクロージャがその分離をキャプチャする
  • 生成するすべての Task { } がその分離を継承する

何もアノテートする必要はない。スレッドについて考える必要はない。コードは MainActor で実行され、分離はプログラム全体に自動的に伝播する。

その継承から抜け出す必要があるときは、明示的にする:

  • @concurrent は「バックグラウンドスレッドで実行」と言う
  • actor は「この型は独自の分離ドメインを持つ」と言う
  • Task.detached { } は「ゼロから始める、何も継承しない」と言う

そして分離ドメイン間でデータを渡すとき、Swift は安全かチェックする。それが Sendable の役割だ: 境界を安全に越えられる型をマークする。

それだけだ。モデル全体:

  1. 分離は伝播する - MainActor からコードを通じて
  2. 明示的にオプトアウトする - バックグラウンド作業や別の状態が必要なとき
  3. Sendable が境界を守る - データがドメイン間を越えるとき

コンパイラが文句を言うとき、これらのルールのどれかが違反されたと伝えている。継承をトレースしよう: 分離はどこから来た?コードはどこで実行しようとしている?どんなデータが境界を越えている?正しい質問をすれば答えは通常明らかだ。

ここからどこへ

良いニュース: すべてを一度にマスターする必要はない。

ほとんどのアプリは基本だけで十分だ。 ViewModel に @MainActor をマークし、ネットワーク呼び出しに async/await を使い、ボタンタップから非同期作業を開始するときに Task { } を作成する。それだけだ。これが現実のアプリの 80% をカバーする。コンパイラがもっと必要か教えてくれる。

並列作業が必要なときは、複数のものを一度に取得するために async let を使うか、タスク数が動的な場合は TaskGroup を使う。キャンセルを優雅に処理することを学ぼう。これが複雑なデータ読み込みやリアルタイム機能を持つアプリをカバーする。

高度なパターンは後で来る - もし来るなら。共有可変状態のためのカスタムアクター、CPU 集約的処理のための @concurrent、深い Sendable の理解。これはフレームワークコード、サーバーサイド Swift、複雑なデスクトップアプリだ。ほとんどの開発者はこのレベルを必要としない。

シンプルに始める

持っていない問題のために最適化するな。基本から始め、アプリを出荷し、実際の問題にぶつかったときだけ複雑さを追加しよう。コンパイラが導いてくれる。

注意: よくある間違い

async = バックグラウンドと思う

// これはまだメインスレッドをブロックする!
@MainActor
func slowFunction() async {
    let result = expensiveCalculation()  // 同期作業 = ブロック
    data = result
}

async は「一時停止できる」という意味だ。実際の作業はそれが実行される場所で実行される。CPU 重い作業には @concurrent(Swift 6.2)か Task.detached を使おう。

アクターを作りすぎる

// 過剰設計
actor NetworkManager { }
actor CacheManager { }
actor DataManager { }

// より良い - ほとんどは MainActor で済む
@MainActor
class AppState { }

カスタムアクターが必要なのは、MainActor に置けない共有可変状態があるときだけだ。Matt Massicotte のルール: アクターを導入するのは (1) non-Sendable な状態があり、(2) その状態への操作がアトミックでなければならず、(3) それらの操作が既存のアクターで実行できない場合のみ。正当化できないなら、代わりに @MainActor を使おう。

すべてを Sendable にする

すべてが境界を越える必要はない。あちこちで @unchecked Sendable を追加しているなら、一歩引いてそのデータが本当に分離ドメイン間を移動する必要があるか問おう。

必要ないのに MainActor.run を使う

// 不要
Task {
    let data = await fetchData()
    await MainActor.run {
        self.data = data
    }
}

// より良い - 関数を @MainActor にする
@MainActor
func loadData() async {
    self.data = await fetchData()
}

MainActor.run が正解なことはまれだ。MainActor 分離が必要なら、代わりに関数に @MainActor をアノテートしよう。より明確で、コンパイラがもっと助けてくれる。Matt のこれについての見解を参照。

協調スレッドプールをブロックする

// 絶対にやるな - デッドロックのリスク
func badIdea() async {
    let semaphore = DispatchSemaphore(value: 0)
    Task {
        await doWork()
        semaphore.signal()
    }
    semaphore.wait()  // 協調スレッドをブロック!
}

Swift の協調スレッドプールは限られたスレッドを持つ。DispatchSemaphoreDispatchGroup.wait() などでブロックするとデッドロックを引き起こす可能性がある。同期と非同期のコードをブリッジする必要があるなら、async let を使うか、完全に非同期のままになるように再構成しよう。

管理されないタスクを作る

Task { ... }Task.detached { ... } で手動で作成するタスクは管理されない。管理されないタスクを作成した後、それらを制御することはできない。開始したタスクがキャンセルされてもキャンセルできない。作業が完了したか、エラーをスローしたか、戻り値を収集することもできない。このようなタスクを開始することは、ボトルを海に投げてメッセージが届くことを願うようなもので、そのボトルを二度と見ることはない。

オフィスビル

Task は従業員に仕事を割り当てるようなものだ。従業員はリクエストを処理し(他のオフィスを待つことを含む)、あなたは目の前の仕事を続ける。

従業員に仕事を送った後、彼女と通信する手段はない。仕事を止めるよう伝えることも、完了したかどうか、その仕事の結果が何だったかを知ることもできない。

本当に欲しいのは、従業員にトランシーバーを渡して、リクエストを処理している間も通信できるようにすることだ。トランシーバーがあれば、止まるよう伝えることができ、彼女はエラーに遭遇したときに伝えることができ、あなたが与えたリクエストの結果を報告することができる。

管理されないタスクを作成する代わりに、Swift の並行処理を使って作成するサブタスクの制御を維持しよう。TaskGroup を使って(グループの)サブタスクを管理しよう。Swift は withTaskGroup() { group in ... } 関数をいくつか提供している。

func doWork() async {

    // すべてのサブタスクが戻る、エラーをスローする、またはキャンセルされたときにこれは戻る
    let result = try await withThrowingTaskGroup() { group in
        group.addTask {
            try await self.performAsyncOperation1()
        }
        group.addTask {
            try await self.performAsyncOperation2()
        }
        // ここでタスクの結果を待って収集する
    }
}

func performAsyncOperation1() async throws -> Int {
    return 1
}
func performAsyncOperation2() async throws -> Int {
    return 2
}

グループの子タスクの結果を収集するには、for-await-in ループを使える:

var sum = 0
for await result in group {
    sum += result
}
// sum == 3

TaskGroup について詳しくは Swift ドキュメントを参照。

タスクと SwiftUI についての注意

UI を書くとき、同期コンテキストから非同期タスクを開始したいことがよくある。例えば、UI 要素のタッチに応答して非同期で画像を読み込みたい。Swift では同期コンテキストから非同期タスクを開始することはできない。だから Task { ... } を含むソリューションを見かけるが、これは管理されないタスクを導入する。

SwiftUI の同期修飾子から TaskGroup を使うことはできない。withTaskGroup() も async 関数であり、関連する関数も同様だ。

代わりに、SwiftUI は非同期操作を開始するために使える非同期修飾子を提供している。すでに言及した .task { } 修飾子は () async -> Void 関数を受け取り、他の async 関数を呼び出すのに理想的だ。すべての View で利用可能。ビューが表示される前にトリガーされ、作成されるタスクはビューのライフサイクルに管理・バインドされ、ビューが消えるとタスクがキャンセルされる。

タップで画像を読み込む例に戻る: 同期の .onTap() { ... } 関数から非同期の loadImage() 関数を呼び出すために管理されないタスクを作成する代わりに、タップジェスチャーでフラグをトグルし、task(id:) 修飾子を使って id(フラグ)の値が変わったときに非同期で画像を読み込むことができる。

例を示す:

struct ContentView: View {

    @State private var shouldLoadImage = false

    var body: some View {
        Button("クリック!") {
            // フラグをトグル
            shouldLoadImage = !shouldLoadImage
        }
        // View がサブタスクを管理する
        // ビューが表示される前に開始
        // ビューが非表示になると停止
        .task(id: shouldLoadImage) {
            // フラグの値が変わると、SwiftUI はタスクを再開する
            guard shouldLoadImage else { return }
            await loadImage()
        }
    }
}

チートシート: クイックリファレンス

キーワード 何をするか
async 関数は一時停止できる
await 終わるまでここで一時停止
Task { } 非同期作業を開始、コンテキストを継承
Task.detached { } 非同期作業を開始、コンテキスト継承なし
@MainActor メインスレッドで実行
actor 分離された可変状態を持つ型
nonisolated アクター分離をオプトアウト
nonisolated(nonsending) 呼び出し元のエクゼキュータに留まる
Sendable 分離ドメイン間で渡しても安全
@concurrent 常にバックグラウンドで実行(Swift 6.2+)
#isolation 呼び出し元の分離をパラメータとしてキャプチャ
async let 並列作業を開始
TaskGroup 動的な並列作業

参考資料

Matt Massicotte のブログ(強く推奨)

ツール

  • Tuist - 大規模なチームとコードベースでより速く開発

AI エージェントスキル

AI コーディングアシスタントに Swift Concurrency を理解させたいですか?Claude Code、Codex、Amp、OpenCode などの AI エージェント向けに、これらのメンタルモデルをパッケージ化した SKILL.md ファイルを提供しています。

その他のスキル

スキルとは?

スキルは AI コーディングエージェントに専門知識を教える markdown ファイルです。Swift Concurrency スキルをエージェントに追加すると、非同期 Swift コードの作成を支援する際にこれらの概念を自動的に適用します。

使い方

エージェントを選択してコマンドを実行してください:

# 個人スキル(すべてのプロジェクト)
mkdir -p ~/.claude/skills/swift-concurrency
curl -o ~/.claude/skills/swift-concurrency/SKILL.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# プロジェクトスキル(このプロジェクトのみ)
mkdir -p .claude/skills/swift-concurrency
curl -o .claude/skills/swift-concurrency/SKILL.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# グローバル指示(すべてのプロジェクト)
curl -o ~/.codex/AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# プロジェクト指示(このプロジェクトのみ)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# プロジェクト指示(推奨)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# グローバルルール(すべてのプロジェクト)
mkdir -p ~/.config/opencode
curl -o ~/.config/opencode/AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# プロジェクトルール(このプロジェクトのみ)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md

このスキルには、オフィスビルの比喩、隔離パターン、Sendable ガイド、よくある間違い、クイックリファレンス表が含まれています。Swift Concurrency コードを扱う際に、エージェントがこの知識を自動的に活用します。