0
1

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】async letの基本的な使い方

Last updated at Posted at 2024-12-15

async letが導入された背景

async letが導入される前、非同期処理を並列で実行するにはTaskGroupを使う必要がありました。

let result = await withTaskGroup(of: Int.self) { group -> Int in
        // 複数のタスクを追加
        group.addTask { await fetchNumber(1) }
        group.addTask { await fetchNumber(2) }
        group.addTask { await fetchNumber(3) }

        // ...
}

しかしTaskGroupにはいくつかの問題点がありました。

  • 異なる型の結果を扱う際コードが冗長になる
  • ただ子タスクが親タスクに単一の値を返せばいいだけの場合でも、冗長なボイラーコードが必要

その問題を解決するため、簡単に非同期の並列処理を行えるasync letが導入されました。

async letの使い方

async letは宣言の左部分に付属させるだけで使用できます。

async let value = fetchData()

この式で宣言された時点で、子タスクとしてfetchDataが非同期で処理されるようになります。

このvalueの値を使いたい時は、awaitを使用する必要があります。逆にawaitを使うまでは、処理は中断されません。

print(await value)

もしthrowsがついた関数を並列で処理したい場合は、結果を取得する際にtryをつける必要があります。

async let result = fetchData() // fetchDataはthrowsがついた関数
print(try await result)

TaskGroupが抱えていた問題を解決できたのか?

先述した通りTaskGroupには、異なる型の結果を扱う際コードが冗長になるという問題点がありました。

例えば以下のコードのような場合です。

// 異なる型の結果を扱うための列挙型
enum FetchResult {
    case number(Int)
    case text(String)
}

func fetchNumber() async throws -> Int {
    try await Task.sleep(for: .seconds(1))
    return 42
}

func fetchText() async throws -> String {
    try await Task.sleep(for: .seconds(1))
    return "Hello, World!"
}

// TaskGroup を使用して異なる型の結果を取得
func fetchAllWithTaskGroup() async throws -> (Int, String) {
    var number: Int?
    var text: String?
    
    try await withThrowingTaskGroup(of: FetchResult.self) { group in
        group.addTask {
            return .number(try await fetchNumber())
        }
        group.addTask {
            return .text(try await fetchText())
        }
        
        for try await result in group {
            switch result {
            case .number(let fetchedNumber):
                number = fetchedNumber
            case .text(let fetchedText):
                text = fetchedText
            }
        }
    }
    
    return (number!, text!)
}

Task {
    do {
        let (number, text) = try await fetchAllWithTaskGroup()
        print("Number: \(number)")
        print("Text: \(text)")
    } catch {
        print("エラーが発生しました: \(error)")
    }
}

Stringが返り値のfetchTextと、Intが返り値のfetchNumberを並列で処理するために、FetchResultを定義しています。ぱっと見かなり複雑で読みづらいコードだと思います。

こちらのコードをasync letを使って描き直したのがこちらです。

func fetchNumber() async throws -> Int {
    try await Task.sleep(for: .seconds(1))
    return 42
}

func fetchText() async throws -> String {
    try await Task.sleep(for: .seconds(1))
    return "Hello, World!"
}

// async let を使用して異なる型の結果を取得
func fetchAllWithAsyncLet() async throws -> (Int, String) {
    // 並行してタスクを開始
    async let number = fetchNumber()
    async let text = fetchText()
    
    // 結果を待機して取得
    let finalNumber = try await number
    let finalText = try await text
    
    return (finalNumber, finalText)
}

// 使用例
Task {
    do {
        let (number, text) = try await fetchAllWithAsyncLet()
        print("Number: \(number)")
        print("Text: \(text)")
    } catch {
        print("エラーが発生しました: \(error)")
    }
}

かなりシンプルで読みやすくなったと思います。

  • 並列処理したい関数の戻り値が異なる型の場合
  • 並列で処理したいタスクの個数が決まっている場合

これらの場合は、async letを使って記述するとシンプルに書けるようになると思います。

async letで生成されたタスクのキャンセルについて

async letで生成されるタスクは呼び出し元の親タスクの子タスクとして生成されます。
そのため親タスクがキャンセルされると、async letの処理もキャンセルされることになります。

以下は並列処理中に親タスクがキャンセルされる例です。

func longRunningTask(label: String) async -> Int {
    do {
        for i in 1...5 {
            try Task.checkCancellation() // キャンセルをチェック
            print("\(label): Running step \(i)")
            try await Task.sleep(for: .seconds(1)) // 1秒待機
        }
        print("\(label): Completed")
        return 42
    } catch {
        print("\(label): Cancelled")
        return -1
    }
}

func main() async {
    async let taskA = longRunningTask(label: "Task A")
    async let taskB = longRunningTask(label: "Task B")

    print("Parent task: Starting child tasks...")

    let values = await(taskA, taskB)
    print(values)
}

let parent = Task {
    await main()
}

// 親タスクを2.5秒後にキャンセル
Task {
    try? await Task.sleep(for: .seconds(2.5)) // 2.5秒待機
    parent.cancel()
    print("Parent task: Cancelled")
}

実行結果(順番は前後する可能性がある)

Parent task: Starting child tasks...
Task A: Running step 1
Task B: Running step 1
Task A: Running step 2
Task B: Running step 2
Task B: Running step 3
Task A: Running step 3
Task B: Cancelled
Parent task: Cancelled
Task A: Cancelled
(-1, -1)

実行結果を見るとわかる通り、親タスクがキャンセルされると、async letで定義した処理もキャンセルされています。

参考にした記事

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?