Contents 非表示
TaskGroupとは何か
TaskGroup
(または withThrowingTaskGroup
)= 動的に子タスクを組める並列処理のコンテナ、子タスクをどんどん追加して、全部完了するまで待つ仕組み。
画像を大量にダウンロードする時。URLリストが動的でも、forループで addTask
して一緒に並列処理できるのが強み。
async let との違い
比較軸 | async let | TaskGroup |
---|---|---|
タスク数 | 書く前に数が確定している必要がある | 動的に必要なだけ子タスクを追加できる |
構文 | 簡潔で使いやすい | やや複雑だけど柔軟性が高い |
エラー/キャンセル | 片方が失敗したら全体キャンセル(即) | 個別に制御やキャンセル可能 |
結果取得 | まとめて await する | for await … in group で逐次取得可能 |
どんな場面で使うか:
- 処理数が動的 → APIの応答数に応じて処理数が変わる時
- キャンセル条件がある → 一定数成功したら他を止めたい時など
- 大量並列処理 → 100 件以上とか大量データを非同期で処理したい時
サンプルコード
import SwiftUI
import UIKit
struct HomeView: View {
@State private var images: [UIImage] = []
@State private var isLoading = false
@State private var errorMessage: String?
// 画像のURLリスト, ランダムなIDを使用して、ランダムな画像を生成
private let imageURLs: [URL] = (1...10).compactMap { _ in
let randomID = Int.random(in: 1...1_000)
return URL(string: "https://picsum.photos/id/\(randomID)/160/160")
}
var body: some View {
NavigationView {
VStack(spacing: 16) {
if isLoading {
ProgressView("Loading images...")
} else if let errorMessage = errorMessage {
Text(errorMessage)
.foregroundColor(.red)
.padding()
} else {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(Array(images.enumerated()), id: \.offset) { index, image in
Image(uiImage: image)
.resizable()
.frame(width: 160, height: 160)
.cornerRadius(12)
}
}
.padding(.horizontal)
}
}
Button("Download Images") {
Task {
await downloadAllImages()
}
}
.padding()
}
.navigationTitle("TaskGroup Demo")
}
}
// TaskGroupで画像を非同期で取得
private func downloadAllImages() async {
isLoading = true
errorMessage = nil
images = []
do {
let downloadedImages = try await withThrowingTaskGroup(of: UIImage?.self) { group in
// 各URLから画像を非同期でダウンロード
for url in imageURLs {
// TaskGroupにタスクを追加
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
return UIImage(data: data)
}
}
var result: [UIImage] = []
// タスクの結果を集める
for try await image in group {
if let img = image {
result.append(img)
}
}
return result
}
self.images = downloadedImages
isLoading = false
} catch {
// エラー処理
self.errorMessage = "画像の取得に失敗しました: \(error.localizedDescription)"
isLoading = false
}
}
}
取得結果の順序を保ちたい場合
↑のコードでは、処理の完了順にappendされるので順番は考慮されない、順番も考慮したいなら、index付きでaddTaskを行う。
try await withThrowingTaskGroup(of: (Int, UIImage?).self) { group in
for (index, url) in imageURLs.enumerated() {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
let image = UIImage(data: data)
return (index, image) // indexとセットで返す
}
}
var resultArray = Array<UIImage?>(repeating: nil, count: imageURLs.count)
for try await (index, image) in group {
resultArray[index] = image
}
self.images = resultArray.compactMap { $0 }
}
enumerated()
でindexとURLのセットにするgroup.addTask
でタプル(index, image)
を返すresultArray
を先に必要数で初期化して、index指定で代入- 最後に
compactMap
で nil を除去して完成
条件付きで途中キャンセルしたい場合
実務でありがちなこと:
- 10件中、3件成功したらOK。他はキャンセルしたい
- 特定条件(例:最初にエラーが出たら)で全部止めたい
キャンセル処理付き TaskGroup 例:
try await withThrowingTaskGroup(of: UIImage?.self) { group in
for url in imageURLs {
group.addTask {
if Task.isCancelled { return nil }
let (data, _) = try await URLSession.shared.data(from: url)
return UIImage(data: data)
}
}
var downloaded: [UIImage] = []
for try await image in group {
if let img = image {
downloaded.append(img)
}
if downloaded.count >= 5 {
group.cancelAll() // 一定数ダウンロードしたら残りをキャンセル
}
}
self.images = downloaded
}
まとめ
- TaskGroup は、動的に非同期タスクを並列実行できる超便利な仕組み
async let
より柔軟で、大量処理や順序制御に向いてるaddTask
で処理をどんどん追加して、for await
で結果を回収withThrowingTaskGroup
は 1件でも throw したら全体キャンセル
パターン | 使いどころ |
---|---|
通常の TaskGroup | 複数処理を一気に並列したいとき |
順番を保つ TaskGroup | UIで順序が重要なとき(例:画像表示) |
条件付きキャンセル TaskGroup | 「3件成功したら十分」「最初の失敗で中断」みたいな制御が必要なとき |
いきなり全てを覚えようとするとパンクするので、分割して理解する、これ大事。
↓ 次回
