🏫 放課後のプログラミング部
登場人物
- 先生:プログラミング部の指導教員
- ユウタ:高校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呼び出しを並行で処理し、最後に結果をまとめて返せる
ユウタ「今日もよく分かりました!」
先生「これで並列処理の幅が広がるね!」