0
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swift Concurrencyに関して勉強したことを料理人を登場させて理解を深める

Last updated at Posted at 2025-04-12

0.はじめに

モバイルアプリエンジニアに興味をもち、Swiftの学習を進めている大学院生です!
Qiita初投稿です。間違っている点や質問がございましたらビシバシお願いします!!

なぜSwift Concurrencyを学ぼうと思ったか

TCAのチュートリアル中に次のような警告が、、、

TCAのチュートリアル
import ComposableArchitecture
import Foundation
import Testing

@testable import TCATutorial
@MainActor
struct NumberFactClient {
    var fetch: @Sendable (Int) async throws -> String
}

extension NumberFactClient: DependencyKey {
    static let liveValue = Self(
        fetch: { number in
            let(data, _) = try await URLSession.shared
                .data(from: URL(string: "http://numbersapi.com/\(number)")!)
                      return String(decoding: data, as: UTF8.self)
        }
    )
}

Main actor-isolated static property 'liveValue' cannot be used to satisfy nonisolated requirement from protocol 'DependencyKey'; this is an error in the Swift 6 language mode

調べたところ、swift6からConcurrencyの扱いが厳しくなっているようです。
https://developer.apple.com/documentation/swift/adoptingswift6

Swift Concurrencyに関しての記事は散々出回っていますが、自分の理解を深めるためにも投稿させていただきます。
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Actors

目次

1. async/await

Swift Concurrencyで1番最初に出会うのが基本的な async awaitです。
まずはサンプルコード。

サンプルコード
import Foundation
func cookPasta() async -> String {
    try? await Task.sleep(for: .seconds(2))
    print("🍝 パスタができた")
    return "🍝"
}

func cookCurry() async -> String {
    try? await Task.sleep(for: .seconds(5))
    print("🍛 カレーができた")
    return "🍛"
}

func makeSalad() -> String {
    print("🥗 サラダを作った")
    return "🥗"
}

func serve(_ curry: String, _ pasta: String, _ salad: String) {
    print("配膳: \(curry), \(pasta), \(salad)")
}

@main
struct Main {
    static func main() async {
        print("🍽 調理スタート")

        let curryResult = await cookCurry()   // カレーを煮込む(5秒)
        let pastaResult = await cookPasta()   // パスタを茹でる(2秒)
        let saladResult = makeSalad()         // サラダはすぐ作れる
        print("🧾 料理の準備完了")
        serve(curryResult, pastaResult, saladResult)
    }
}
実行結果
🍽 調理スタート
🍛 カレーができた
🍝 パスタができた
🥗 サラダを作った
🧾 料理の準備完了
配膳: 🍛, 🍝, 🥗

料理人の動きにたとえるなら…

  • カレーを煮込む → 完成を待ってから…
  • パスタを茹でる → それが終わってから…
  • サラダを作る(これはすぐ終わる)

というように、キッチンでは一人のコックさんが順番に料理している状態です。
async/awaitを使っていても、順番に await すればこのような直列的な動きになります。

2. async let

じゃあ、もし「カレーとパスタを同時に調理」できたらもっと早くなるでしょうか?
async letを使うことで、非同期関数を並行に実行することができます。

サンプルコード
struct Main {
    static func main() async {
        print("🍽 調理スタート")
        
        async let curry = cookCurry()
        async let pasta = cookPasta()
        
        let salad = makeSalad()
        
        let curryResult = await curry
        let pastaResult = await pasta
        
        print("🧾 料理の準備完了")
        serve(curryResult, pastaResult, salad)
        
    }
}

注目してほしいのは、curryResultを先にawaitしているにもかかわらず、先に 🍝 パスタができたが出力されている点です。
これは、async letによって カレーとパスタが同時に調理されているため、それぞれが独立して進行し、早く終わったパスタの出力が先に現れたということです。

出力結果
🍽 調理スタート
🥗 サラダを作った
🍝 パスタができた
🍛 カレーができた
🧾 料理の準備完了
配膳: 🍛, 🍝, 🥗

3. TaskとTask Group

Taskの公式ドキュメント
2章の、async letでは、暗黙的に子タスクを生成しています。
Task Groupでは、並列実行する数を動的に決めることができます。

サンプルコード
import Foundation

func cook(_ dish: String) async -> String {
    let seconds = [
        "カレー": 5,
        "パスタ": 2,
        "サラダ": 1,
        "スープ": 3,
        "ケーキ": 4,
        "ジュース": 1,
        "ピザ": 10
    ][dish, default: 2]

    try? await Task.sleep(for: .seconds(UInt64(seconds)))
    print("\(dish) ができた")
    return dish
}

料理を増やしました。調理時間の異なるメニューを複数用意し、メニューに記載されている料理一つ一つに、withTaskGroup(of: String.self){ group in ...}で、子タスクを集めるグループを作って、クロージャの中で処理してください。というような記述をしています。そして、menuに記載された料理をgroup.addTask{ await cook(dish) }でタスクとして追加しています。

サンプルコード
@main
struct Main {
    static func main() async {
        print("🍽 TaskGroupによる調理スタート")

        let menu = ["カレー", "ピザ", "サラダ", "スープ", "ケーキ"]

        let results = await withTaskGroup(of: String.self) { group in
            for dish in menu {
                group.addTask {
                    await cook(dish)
                }
            }

            var completed: [String] = []
            for await result in group {
                completed.append(result)
            }
            return completed
        }

        print("🧾 全料理完成: \(results.joined(separator: ", "))")
    }
}

completedに完成した順で料理を.appendして、最終的に完成したら全料理完成で表示します。
実行結果を見ると、並行にcook(dish)が実行されたことによって、早くできたものから順にcompleted.appendされているのがわかります。
このように、async letのようにあらかじめ実行するタスクが決まっていない時にTask Groupは効果を発揮します。

実行結果
🍽 TaskGroupによる調理スタート
サラダ ができた
スープ ができた
ケーキ ができた
カレー ができた
ピザ ができた
🧾 全料理完成: サラダ, スープ, ケーキ, カレー, ピザ

4. Task Cancellation

「通信が遅い!」「読み込むデータが重すぎる!」という時にすべてのタスクが完了するのを待たずにユーザーがこの作業を停止できるようにするには、タスクでキャンセルを確認し、キャンセルされた場合は実行を停止する必要があります。Swiftではcooperative cancellation modelを採用しています。cooperative cancellation modelのイメージは以下の通りです。

タスクがキャンセルされたからといって自動で気に止まるのではなく、キャンセルされたかどうかを自分で確認して、適切に止める。

そして、キャンセルの方法は二つあります。
checkCancellationを呼び出すと、タスクがキャンセルされた場合にエラーがスローされます。エラーが伝播することで、タスクの全ての作業を停止できます。

キャンセルその1.Task.checkCancellation()

この処理では、CancellationErrorがthrowされたタイミングでタスクをキャンセルします。throw関数で、中断したい処理を明確に示すときは、こちらを使うのが最適です。

checkCancellation
import Foundation

func cook(_ dish: String) async throws -> String { 
    let seconds = [
        "カレー": 5,
        "パスタ": 2,
        "サラダ": 1,
        "スープ": 3,
        "ケーキ": 4,
        "ジュース": 1,
        "ピザ": 10
    ][dish, default: 2]

    try Task.checkCancellation()
    print("\(dish)の調理を開始")
    
    try await Task.sleep(for: .seconds(UInt64(seconds)))
    print("\(dish) ができた")
    return dish
}

@main
struct Main {
    static func main() async {
        print("🍽 TaskGroupによる調理スタート")

        let menu = ["カレー", "ピザ", "サラダ", "スープ", "ケーキ"]

        let results = await withTaskGroup(of: String?.self) { group in
            for dish in menu {
                if dish == "スープ" {
                    group.cancelAll()
                    print("スープの調理前にキャンセルが実行されました。")
                }
                group.addTask {
                    do{
                        return try await cook(dish)
                    } catch is CancellationError {
                        print("\(dish)の調理はキャンセルされました。")
                        return "\(dish)(キャンセル)"
                    } catch {
                        print("\(dish)で予期せぬエラー : \(error)")
                        return "\(dish) エラー"
                    }
                }
            }

            var completed: [String] = []
            for await result in group {
                if let result {
                    completed.append(result)
                }
            }
            return completed
        }

        print("🧾 完成した料理: \(results.joined(separator: ", "))")
    }
}

実行結果を見ると、キャンセル通知が来たタイミングで、即時実行中のタスクが中断され、出力されていることがわかります。

実行結果
🍽 TaskGroupによる調理スタート
カレーの調理を開始
ピザの調理を開始
サラダの調理を開始
スープの調理前にキャンセルが実行されました。
カレーの調理はキャンセルされました。
ピザの調理はキャンセルされました。
スープの調理はキャンセルされました。
サラダの調理はキャンセルされました。
ケーキの調理はキャンセルされました。
🧾 完成した料理: スープ(キャンセル), ピザ(キャンセル), カレー(キャンセル), サラダ(キャンセル), ケーキ(キャンセル)

キャンセルその2.Task.isCancelled

isCancelledはtruefalseを返すのみの処理です。サンプルコードでは、isCancelledをguard文でチェックして、trueになったタイミングで、groupの子タスクをキャンセルします。つまり、キャンセルされる前の実行済みタスクの値は処理され、出力されるわけです。

isCancelled
        let menu = ["カレー", "ピザ", "サラダ", "スープ", "ケーキ"] //注文リスト
        let results = await withTaskGroup(of: String?.self) { group in
            for dish in menu {
                if dish == "ケーキ" {
                    print("ケーキの調理前にキャンセルが実行されました。")
                    group.cancelAll()
                }
                let added = group.addTaskUnlessCancelled {
                    guard !Task.isCancelled else {
                        print("\(dish)はキャンセルされました。")
                        return nil
                    }
                    return await cook(dish)
                }
                guard added else {
                    print("\(dish)の追加に失敗しました(キャンセル)")
                    break
                }
            }

実行結果を見ると、スープがキャンセルされる前に処理が完了したカレーは完成して、スープがキャンセルされたタイミングでまだ処理中だったメニューが全部キャンセルされています。isCancelledを随時確認することで、キャンセルかどうかをみて、"キャンセルされました"と表示するかどうかを判断しています。

実行結果
🍽 TaskGroupによる調理スタート
スープの調理前にキャンセルが実行されました。
スープの追加に失敗しました(キャンセル)
ケーキの追加に失敗しました(キャンセル)
ピザはキャンセルされました。
サラダはキャンセルされました。
カレー ができた
🧾 完成した料理: カレー

5. Unstructed Concurrency

Swift Concurrencyは、Structured Concurrencyが基本です。これは、1、2、3章のasync letTaskGroupのように「親タスク ⇄ 子タスク」の関係を明示的に持ちます。

それに対して、Unstructured Concurrencyは:

  • 親タスクに属さないタスク

  • 自分自身で完全に制御する必要がある

つまり、より柔軟に、でもより責任を持ってタスクを管理するためのスタイルです。
「ある処理だけ完全にバックグラウンドで動かしたい」「親と切り離した一時的な処理を動かしたい」「Viewが消えても続けたい処理がある」といったときに使われるのがUnstructedConcurrencyです。

サンプルコード
import Foundation

func cook(_ dish: String) async {
    print("🧑‍🍳 \(dish)の調理を開始")
    try? await Task.sleep(for: .seconds(2))
    print("\(dish)が完成しました")
}

@main
struct Main {
    static func main() async {
        print("🍽 調理スタート")

        let menu = ["カレー", "ピザ", "サラダ", "スープ", "ケーキ"]

        Task {
            await cook(menu[0]+"(Task)")
        }
        
        Task.detached {
            await cook(menu[1]+"(Detached)")
        }
        
        try? await Task.sleep(for: .seconds(3))
        print("調理完了")
    }
}
実行結果
🍽 調理スタート
🧑‍🍳 ピザ(Detached)の調理を開始
🧑‍🍳 カレー(Task)の調理を開始
カレー(Task)が完成しました
ピザ(Detached)が完成しました
調理完了

6. Actor

Swift Concurrencyでは データ競合(Data Race) を避けるためにActorという仕組みが用意されています。
料理人の世界で言うと「冷蔵庫」のようなもの

🍽 料理人たちはそれぞれ同時に調理していて、冷蔵庫の中身(在庫)を同時に触ろうとしたら、どちらかが冷蔵庫を離れるまで待ちます!っていうイメージ。
以下のactorで定義しているFridgeには以下の在庫(データ)があります。

  • トマト:3個
  • たまねぎ:2個
    take(item, chef)で、料理人(シェフ)が特定の食材を取り出す処理を担当します。
    この関数を複数のシェフが同時に呼び出しても、actorはアクセスを順番にさばくので、
    在庫データ(stock)を安全に扱うことができます。
actor Fridge {
    private var stock: [String: Int] = ["トマト": 3, "たまねぎ": 2]

    func take(_ item: String, by chef: String) -> Bool {
        print("👨‍🍳 \(chef) が「\(item)」を取りに来た")
        if let count = stock[item], count > 0 {
            stock[item]! -= 1
            print("✅ \(chef) は「\(item)」を取り出した(残り: \(stock[item]!))")
            return true
        } else {
            print("❌ \(chef) は「\(item)」を取れなかった(在庫なし)")
            return false
        }
    }

    func checkStock() -> [String: Int] {
        return stock
    }
}

fridgeactorとして宣言されているので、asyncでシェフたちが同時に冷蔵庫にアクセスしようとしても、順番に冷蔵庫へのアクセスを許可するので、データ競合が起きません。

@main
struct Main {
    static func main() async {
        let fridge = Fridge()

        async let chef1 = fridge.take("トマト", by: "シェフ1")
        async let chef2 = fridge.take("トマト", by: "シェフ2")
        async let chef3 = fridge.take("トマト", by: "シェフ3")

        let results = await [chef1, chef2, chef3]
        print("取り出し結果: \(results)")

        let stock = await fridge.checkStock()
        print("最終在庫: \(stock)")
    }
}

実行すると、それぞれの料理人が別のスレッドから冷蔵庫のアイテムを順番に取っているのがわかります。
Actorを使うことで、データ競合を防ぎながら並行処理を行えます。

実行結果
👨‍🍳 シェフ1 が「トマト」を取りに来た
✅ シェフ1 は「トマト」を取り出した(残り: 2)
👨‍🍳 シェフ3 が「トマト」を取りに来た
✅ シェフ3 は「トマト」を取り出した(残り: 1)
👨‍🍳 シェフ2 が「トマト」を取りに来た
✅ シェフ2 は「トマト」を取り出した(残り: 0)
取り出し結果: [true, true, true]
最終在庫: ["トマト": 0, "たまねぎ": 2]

7. isolatedとnonisolated

概念と使い方

actorは、データ競合を防ぐために1つのタスクずつしかアクセスされない仕組みでした。
ただし、そのままだと、全てが順番待ちになってしまうので、以下のような問題が出てきます。

  • イミュータブルな値を確認したいだけなのに順番待ち
  • 関数を一部だけ切り出して再利用できない

そこで登場するのがisolatednonisolatedです。
Fridgeに新たにその冷蔵庫の識別番号や製造日といった、不変なactor内が持つ変数を追加してみました。

actor Fridge {
    let ID: String
    let modelName: String
    let capacity: Int
    let createdAt: Date

    init(ID: String, modelName: String, capacity: Int, createdAt: Date) {
        self.ID = ID
        self.modelName = modelName
        self.capacity = capacity
        self.createdAt = createdAt
    }

    // ...省略...
    
    // 状態に触れないラベル → nonisolatedでawait不要
    nonisolated func printLabel() {
        print("🧾 製造ラベル: [ ID: \(ID), 機種名: \(modelName), 容量: \(capacity)L, 製造日: \(createdAt.formatted(date: .abbreviated, time: .omitted)) ]")
    }
}

ついでに、isolateの例として、add()関数を用意しました。

    func add(_ item: String, _ count: Int, by supplier: String) {
        print("🚚 \(supplier) が食材を補充した(\(item): \(count)個)")
        stock[item, default: 0] += count
    }

レストランの営業中に電気業者さんが冷蔵庫の定期点検に来ました。
複数人のシェフが順番に食材を冷蔵庫に直しているところに電気業者さんを並ばせて待つわけにはいきませんね。
そもそも冷蔵庫の製造日やIDって、冷蔵庫が作られた時から不変なものなので、データが競合する心配すらありません。イミュータブルで宣言されたプロパティは初期化後に変更されないため、非同期アクセスでもデータ競合が発生しないことが保証されています。
製造業者は在庫状態へはアクセスせずに、冷蔵庫の概要だをみます。こういったものをnonisolatedで管理することで、awaitで待たずにアクセスすることを可能にします。

@main
struct Main {
    static func main() async {
        let fridge = Fridge(
            ID: "SCO1100",
            modelName: "業務用冷蔵庫",
            capacity: 300,
            createdAt: Date()
        )

        async let chef1 = fridge.take("トマト", by: "シェフ1")
        async let chef2 = fridge.take("トマト", by: "シェフ2")
        async let chef3 = fridge.take("たまねぎ", by: "シェフ3")

        let results = await [chef1, chef2, chef3]
        print(results)
        print("\n🔧 製造業者が点検にやってきた。「ラベル見せてくれる?」")
        fridge.printLabel()  // ← nonisolated なので await 不要
        await fridge.add("トマト", 20, by: "シェフ4")
        print("\n📊 最終在庫状況↓")
        let stock = await fridge.currentStock()
        print("\(stock)")
    }
}

シェフはデータ競合することなく安全に冷蔵庫から食材を取り出したり、追加したりを行い、業者は待たずとも冷蔵庫の情報を取得できます。

実行結果
👨‍🍳 シェフ1 が「トマト」を取りに来た
✅ シェフ1 は「トマト」を取り出した(残り: 2)
👨‍🍳 シェフ2 が「トマト」を取りに来た
✅ シェフ2 は「トマト」を取り出した(残り: 1)
👨‍🍳 シェフ3 が「たまねぎ」を取りに来た
✅ シェフ3 は「たまねぎ」を取り出した(残り: 1)
[true, true, true]

🔧 製造業者が点検にやってきた。「ラベル見せてくれる?」
🧾 製造ラベル: [ ID: SCO1100, 機種名: 業務用冷蔵庫, 容量: 300L, 製造日: 2025年4月12日 ]
🚚 シェフ4 が食材を補充した(トマト: 20個)

📊 最終在庫状況↓
["トマト": 21, "たまねぎ": 1]

外部関数としてactor内の関数を使う

actor内で定義した関数をactorの外で再利用したいとき、普通に書くとisolatedな状態に触れられずにエラーになります。
トマトを仕入れる関数を、add()関数を再利用する形で実装していきます。

func restockTomato(_ count: Int, to fridge: Fridge, by chef: String) {
    fridge.add("トマト", count, by: supplier)
}

@main
struct Main {
    static func main() async {
        let fridge = Fridge()
        restockTomato( 10, to: fridge, by: "トマト運送")  // ← ここでエラーの原因となる関数を呼び出し
    }
}

しかし以下のようなエラーが出ます。

Call to actor-isolated instance method 'add(_:_:by:)' in a synchronous nonisolated context

isolatedじゃない引数からactorにはアクセスできません。そこで、fridgeisolatedをつけてあげることで、actor内のisolatedな関数を再利用することができます。
ここで注意するのは、actorの関数を再利用している、つまり、一度に状態へアクセスできる処理は1つであるということです。利用している部分にはawaitをつける必要があります。

func restockTomato(_ count: Int, to fridge: isolated Fridge, by supplier: String) {
    fridge.add("トマト", count, by: supplier)
}


@main
struct Main {
    static func main() async {
        let fridge = Fridge()
        await restockTomato( 10, to: fridge, by: "トマト運送") //actorのisolatedな関数なのでawaitが必要!!
    }
}
実行結果
🚚 トマト運送 が食材を補充した(トマト: 10個)

8. Sendable Types

https://techblog.zozo.com/entry/swift6-strict-concurrency-checking
まず、SendableTypesについて理解する前に、Concurrency Domainというワードについて説明します。
これは、「Taskactorで並行処理実行の安全が保証されたエリアのくくり」
本記事では、

  • Task → 調理台での調理
  • actor → 冷蔵庫
    としてまとめてきました。この調理台での調理の処理、冷蔵庫の在庫追加、取り出しの処理それぞれをConcurrency Domainといいます。これらは異なるDomain内での処理が行われていますが、以下のように異なるDomainへのデータをやり取りするシチュエーションがあり得ます。
struct Ingredient {
    var name: String
    var amount: Int
}

actor Fridge{

     //...省略...
     
     func store(_ ingredient: Ingredient) {
        print("冷蔵庫: \(ingredient.name)\(ingredient.amount)個補充します")
        stock[ingredient.name, default: 0] += ingredient.amount
    }
}

@main
struct Main {
    Task.detached {
        let tomato = Ingredient(name: "トマト", amount: 5)
        await fridge.store(tomato) // ← Task → Actor にデータを渡してる!
    }
}

このようなとき、もしtomatoが変更可能であったり、複数人の人が同時に触るデータであったとき、

「誰かがトマトの数を減らしている間に、別の誰かが数を参照した」
という状況が起こり得ます。これをデータ競合といいます。

これは実際の開発では

  • アプリがクラッシュする
  • データの整合性が崩れる
  • 予測不能な動作になる
    といったバグの温床になりかねません。

Swiftでは、Concurrency Domainを跨いだやり取りしてもデータ競合が起きない保証があるものをSendableを準拠させることで作れます。
Sendableプロトコルがに準拠させることができる条件

  • 値型(structやenumなど)であり、変更可能な状態がSendableなデータで構成されていること。
    • 例: 全てのプロパティがSendableな構造体や、Sendableな関係値を持つ列挙型など。
  • 変更可能な状態を一切持っていない。不変な状態が他のSendableなデータで構成されていること。
    • 例: let定義のみの構造体。
  • 変更可能な状態の安全性が、コードによって保証されている。
    • 例: @MainActorがつけられたクラスや、特定のスレッドやキュー状でプロパティへのアクセスを直列化しているクラスなど。
      これらに該当する型は、Swiftで安全にConcurrency Domain間でやり取りできると認識され、Sendable として扱うことができます。

10 まとめ

async/await
async let
TaskとTask Group
nonisolated
Sendable

0
5
0

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
0
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?