0
0

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】最速バトンリレー!TaskGroupのnext()で見る並列処理の真髄

Last updated at Posted at 2025-02-23

🏫 放課後のプログラミング部

登場人物

  • 先生:プログラミング部の指導教員
  • ユウタ:高校2年生(生徒)
  • アヤカ:高校2年生(生徒)、プログラミング経験あり

シーン1: group.next()for await in の違いを知る

放課後の部室。先生がホワイトボードを使って説明をはじめる。

先生「みんな、前に withTaskGroup を使って並列処理ができるのをやったよね。 for await in group を使うと、子タスクの結果を簡単に全部まとめて受け取れたけれど、今日はもう一つの取り方を紹介するよ。それが group.next() なんだ。」

ユウタ「えっ、group.next() って何ですか? for await in group とどう違うんです?」

先生for await in group は、グループ内で完了した子タスク全てを順番に受け取る手段だったよね。でも group.next()1回の呼び出しで1つずつ、先に終わったタスクから順番に取り出すことができるんだ。」

アヤカ「なるほど! for await in group だと、全部の子タスク結果を同じ構文の中で処理する形だったけど、group.next() なら結果を個別に取り出すことができるってことですね。」


シーン2: group.next() の使い方

先生はホワイトボードにサンプルコードを書き始める。

// TaskGroupView.swift
// ふたつの子タスクのうち最初に終わったタスク結果を得る
guard let firstResult = await group.next() else {
    group.cancelAll()
    return
}
switch firstResult {
case .articles(let a):
    articles = a
case .friends(let f):
    friends = f
}

// 2番目に終わった結果を得る
guard let secondResult = await group.next() else {
    group.cancelAll()
    return
}
switch secondResult {
case .articles(let a):
    articles = a
case .friends(let f):
    friends = f
}

先生「このコードでは、2つの子タスクを作っておいて、group.next()先に終わったほうのタスク結果を最初に取り出しているよ。もし group.next()nil を返したら、つまりもう結果が取れない状態だから、group.cancelAll() で残りのタスクをキャンセルしてリターンさせてる。」

ユウタswitch文で articles とか friends っていうのを見分けて、どっちの結果か判定してるんですね。」

先生「そう。これは列挙型で返ってきていて、どちらのパターンなのかを switch で振り分けている形だ。こうして一つ目と二つ目の結果を順々に取り出して処理するんだよ。」


シーン3: group.next() のメリットとキャンセル

先生がさらにコードを書き足す。

guard let firstResult = await group.next() else {
    group.cancelAll()
    return
}

// ...初めの結果が想定外だったらキャンセルする...
group.cancelAll()

先生「実は group.next() を使うと、こうやって最初に取得した結果次第で残りの子タスクをキャンセルする、なんて処理もできるんだ。for await in group だとまとめて受け取るから、こういった柔軟な制御が少しやりにくいんだよね。」

ユウタ「なるほど…例えば、APIのレスポンスが『エラーだったらもう他のタスクも全部止める』みたいなことが簡単にできる、ってことですね!」

先生「そういうこと。もし最初のタスクから受け取った情報が『もう十分だ』となったなら、他のタスクは無駄になるかもしれない。そんな時に group.cancelAll() が役立つわけだ。」


シーン4: 動的な数の並列処理を実行する

アヤカ「先生、たとえば友達のIDをたくさん持ってる場合に、それぞれのアバター画像をまとめて取りに行きたいことがあるんですけど、どうすればいいですか?」

先生「その場合はループを使って group.addTask をどんどん追加すればOKだよ。ちょうどサンプルがあるから見てみよう。」

先生がホワイトボードにサンプルを示す。

func fetchFriendsAvators(ids: [String]) async -> [String: UIImage?] {
    return await withTaskGroup(of: (String, UIImage?).self) { group in
        for id in ids {
            group.addTask { [weak self] in
                return (id, await self?.fetchAvatorImage(id: id))
            }
        }
        var avators: [String: UIImage?] = [:]
        for await (id, image) in group {
            avators[id] = image
        }
        return avators
    }
}

ユウタ「えっ、こんな風に for id in ids の中で group.addTask を書いていいんですね!」

先生「そう。ここでは ids の数だけ子タスクを追加してるんだ。タスクの戻り値の型を (String, UIImage?) にしておけば、受け取り側でも (id, image) みたいにタプルで取り出せるよ。」


シーン5: 理解を深めるまとめ

先生「今日は大きく分けて2つの話をしたよ。1つ目は group.next() を使って、先に終わったタスクの結果を1つずつ取り出せるってこと。状況によっては、結果を見てから group.cancelAll() することで無駄なタスクを止めるような柔軟な制御ができる。」

ユウタfor await in group との違い、よく分かりました。あっちは全部終わるまでの結果を一気に受け取る感じで、group.next() は1個ずつ確認していくイメージですね。」

先生「そう。2つ目は動的な数の並列処理。配列の要素分だけ group.addTask すれば、同時にたくさんの処理を走らせられる。受け取りは for await (id, image) in group でタプルを受け取って辞書に格納する形。これで効率的に一斉ダウンロードができるよ。」


最後に

  • group.next():子タスクを一つずつ先に終わった順に取り出す

    • メリット:結果次第で cancelAll() などをして、柔軟にコントロールできる
    • for await in group とは異なる使い方が可能
  • withTaskGroup のループ利用:配列などの要素数に応じて動的に子タスクを追加

    • ダウンロードやAPI呼び出しを並行で処理し、最後に結果をまとめて返せる

ユウタ「今日もよく分かりました!」

先生「これで並列処理の幅が広がるね!」

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?