はじめに
なぜ今、DispatchQueueを復習するのか
「Swift Concurrency(async/await)出たし、もうDispatchQueue不要?」
と思うかもしれないけど、実務だと全然通用しない場面あるので改めて復習しておくとGoodだぞという試み
たしかに async/await
はキレイでモダン。でも、
- 既存コードは DispatchQueue ゴリゴリ
- SwiftUIでも
.task {}
の中で使うこと多い - 低レベルな制御(QoSとか、キャンセル、通知)したい時は未だに便利
- 面接で「メインスレッドってなに?」って聞かれる
こういう現場・面接・デバッグの瞬間に、頭の片隅に GCD(Grand Central Dispatch)が入ってるかで差が出る。
Swift Concurrencyとの使い分けも踏まえて
今は「使い分けの時代」。
どっちか片方だけで完結するプロジェクトって、少ないかも。
やりたいこと | Swift Concurrency | DispatchQueue |
---|---|---|
複雑な並列処理 | △(できるけど見にくくなる) | ◎ |
UI更新 | ◎(@MainActorなど) | ◎ |
QoSの細かい制御 | × | ◎ |
キャンセル処理 | △(Task.cancel) | ◎(WorkItem.cancel) |
既存コードの互換 | △(書き換え必要) | ◎ |
特に、SwiftUIプロジェクトでも「Viewではasync/await、ModelではDispatchQueue」みたいなハイブリッド構成、全然アリ。
このブログでは、SwiftUIと絡めた現代的な使い方を中心に、DispatchQueueを復習していく。
DispatchQueueの基本
GCD(Grand Central Dispatch)とは
GCD = Appleが用意してくれてるスレッド管理のための仕組み。
複雑なマルチスレッド処理を簡単に書けるようにしてくれてる。
昔はNSThread
とかでゴリゴリ書いてたけど、今は99% GCDかSwift ConcurrencyでOK。
ポイント:
- スレッド直接触らずに非同期処理できる
- パフォーマンス最適化されたスレッドプールを内部で使ってる
- キュー(Queue)という単位で処理を順番に管理できる
Serial vs Concurrent キュー
キューの種類 | 特徴 |
---|---|
Serial(直列) | 1つずつ順番に処理される(順番保証あり) |
Concurrent(並列) | 複数の処理が同時に走る(順番保証なし) |
// MARK: - 直列キュー(Serial Queue)例
let serialQueue = DispatchQueue(label: "com.taro.serial")
serialQueue.async {
print("🔥 Task 1")
}
serialQueue.async {
print("🔥 Task 2")
}
serialQueue.async {
print("🔥 Task 3")
}
// 出力順番:
// 🔥 Task 1
// 🔥 Task 2
// 🔥 Task 3
// MARK: - 並列キュー(Concurrent Queue)例
// Point: 並列キューでは順番は保証されないけど、複数のタスクが同時に実行されるから処理は速い。
let concurrentQueue = DispatchQueue(label: "com.bigbro.concurrent", attributes: .concurrent)
concurrentQueue.async {
print("🚀 Task A")
}
concurrentQueue.async {
print("🚀 Task B")
}
concurrentQueue.async {
print("🚀 Task C")
}
// 出力順番(例):
// 🚀 Task B
// 🚀 Task A
// 🚀 Task C
// (順番バラバラになる可能性大)
同期(sync) vs 非同期(async)の違い:
種類 | 説明 |
---|---|
.sync {} | 処理が終わるまで待つ(ブロッキング) |
.async {} | 処理を投げてすぐ戻る(非ブロッキング) |
// MARK: - 同期(sync)の例
// Point: .sync はブロックが終わるまで次の行に進まない。
print("🔹 Start")
DispatchQueue.global().sync {
print("🔹 Inside sync block")
}
print("🔹 End")
// 実行順:
// 🔹 Start
// 🔹 Inside sync block
// 🔹 End
// MARK: - 非同期(async)の例
// Point: .async は投げっぱなしなので、次の処理がすぐ動く。UI処理では基本こっちを使う。
print("🔸 Start")
DispatchQueue.global().async {
print("🔸 Inside async block")
}
print("🔸 End")
// 実行順:
// 🔸 Start
// 🔸 End
// 🔸 Inside async block
// ☠️ 注意: メインスレッドで sync すると死ぬ
// NG例:メインスレッドでメインキューにsync
DispatchQueue.main.sync {
print("❌ これはデッドロックします")
}
よく使うDispatchQueueの書き方
メインスレッドでのUI更新
UIの更新は必ずメインスレッドで行わないとクラッシュする可能性あり
DispatchQueue.main.async {
// ✅ UIの更新はここで
self.text = "ロード完了!"
}
// SwiftUIなら .task 内でもこれやる必要がある場面ある(例:非同期API→UI反映)。
グローバルキューでの重い処理
重たい処理(画像処理、データパース、APIコールなど)はバックグラウンドで投げるのが鉄則
DispatchQueue.global(qos: .userInitiated).async {
let result = heavyCalculation()
DispatchQueue.main.async {
// ✅ メインスレッドでUI更新
self.output = result
}
}
QoS | 目的 |
---|---|
.userInteractive | 最優先(即UI反映) |
.userInitiated | 高優先(操作に即反応) |
.utility | 長時間かかる処理(例:ダウンロード) |
.background | 最低優先(例:バックアップ) |
QoS使うことで、OSがいい感じにスケジューリングしてくれる。これ地味に重要。
DispatchWorkItemの活用
WorkItemは、「処理のキャンセルや、完了後に通知したいとき」に使う。
let work = DispatchWorkItem {
print("🧱 実行された処理")
}
DispatchQueue.global().async(execute: work)
// キャンセル可能
let work = DispatchWorkItem {
if work.isCancelled {
print("🙅♂️ キャンセルされました")
return
}
print("✅ 実行される処理")
}
DispatchQueue.global().async(execute: work)
// 何かの条件でキャンセル
work.cancel()
// 処理後に通知
let task = DispatchWorkItem {
print("🔧 重い処理完了")
}
DispatchQueue.global().async(execute: task)
// これで非同期処理→完了→UI更新がキレイに書ける
task.notify(queue: .main) {
print("🎉 UI更新など通知処理")
}
まとめ:
やりたいこと | 書き方 |
---|---|
UI更新 | DispatchQueue.main.async |
重い処理 | DispatchQueue.global(qos: .userInitiated).async |
独自順序制御 | DispatchQueue(label:) |
並列高速処理 | DispatchQueue(label:, attributes: .concurrent) |
キャンセル可処理 | DispatchWorkItem |
SwiftUIとDispatchQueue
SwiftUIでも、時間のかかる処理をバックグラウンドで回して、終わったらUI更新ってパターン、時折ある。
struct ContentView: View {
@State private var result: String = "読み込み中..."
var body: some View {
VStack {
Text(result)
.padding()
Button("重い処理開始") {
loadHeavyStuff()
}
}
}
func loadHeavyStuff() {
// バックグラウンドで重い処理
DispatchQueue.global(qos: .userInitiated).async {
let data = heavyCalculation()
// メインスレッドでUI更新
DispatchQueue.main.async {
result = data
}
}
}
func heavyCalculation() -> String {
sleep(2) // 仮に重い処理として2秒待機
return "処理完了!"
}
}
.taskとDispatchQueueの違い
SwiftUI では .task {}
をよく使うけど、これは async/await
ベース。DispatchQueue
とは立ち位置がちょっと違う。
Text("読み込み中…")
.task {
let result = await loadData()
text = result
}
比較項目 | .task {} | DispatchQueue |
---|---|---|
モダンな書き方 | ◎ | ◯ |
Swift Concurrency前提 | ◎ | ✕ |
SwiftUIに最適化 | ◎ | △(使えるけど気をつけて) |
キャンセル対応 | ◯(Task.cancel) | ◎(WorkItem.cancel) |
QoS制御 | ✕ | ◎ |
非同期API対応 | ◎ | △(自前でクロージャ管理) |
SwiftUIのView直下なら.task
、Modelや処理層ではDispatchQueueって分けるのがGoodかも
SwiftUIで意図せずクラッシュするパターン
// ❌ UI更新をバックグラウンドスレッドでやってしまう
DispatchQueue.global().async {
self.text = "更新しちゃうよ"
}
// ✅ 正しいやり方
DispatchQueue.global().async {
let result = loadStuff()
DispatchQueue.main.async {
self.text = result // UIは必ずメインスレッドで!
}
}
Swift Concurrencyとの使い分け
Swift Concurrency利点・弱点
強み:
- 可読性バツグン(ネスト地獄回避)
- SwiftUIとの親和性◎(
.task
,@MainActor
,Task {}
) - 非同期関数のテストもしやすい
- 複雑な非同期処理も直感的に書ける
// 見やすい非同期コード
func fetch() async {
let data = await fetchData()
let image = await processImage(data)
await updateUI(image)
}
弱点:
- 古いAPIやクロージャ型に弱い(互換性△)
- 処理の優先度(QoS)設定できない
- Swift 5.5+ 対応のみ(古い環境では使えない、さすがに今の時点で問題ない気もしますが)
- 実行中タスクのキャンセルにややクセあり
DispatchQueue利点・弱点
強み:
- 低レベルな制御ができる(QoS、WorkItem、並列制御)
- 古いAPIでも問題なし(クロージャベース)
- Swiftバージョンに縛られにくい
- タスクのキャンセルや通知の自由度が高い
- DispatchGroup / Semaphore など並列・同期系ツールが豊富
弱点:
- SwiftUIとはやや相性悪い(ミスるとクラッシュ)
- ネストが深くなりがち(callback hell)
- 明示的にメインスレッド制御しないと事故る
actor / @MainActorとの関係
actorとは
actor
はスレッドセーフなクラスの進化版。
内部の状態を同時にアクセスされないように保証してくれる。
actor Counter {
private var count = 0
func increment() {
count += 1
}
func get() -> Int {
return count
}
}
@MainActorとは
UI更新を保証してくれる属性。DispatchQueue.main.async {}
のSwift Concurrency版の思想。
@MainActor
class ViewModel: ObservableObject {
@Published var message = ""
func update() {
Task {
let result = await fetchMessage()
message = result // ✅ これはメインスレッド確定
}
}
}
DispatchQueueでやるべき場面
シチュエーション | 理由 |
---|---|
古いクロージャベースのAPIを使う時 | async/awaitに書き換え不要でそのまま使える |
優先度(QoS)を指定したい時 | ConcurrencyにはQoS制御がない |
複数タスクを並列にガンガン回す時 | concurrentPerform や DispatchGroupが強い |
タスクの途中キャンセルが必要な時 | WorkItemで柔軟に対応できる |
スレッドを明示的に制御したい時 | メイン・バックグラウンドの切り替えが簡単 |
ViewModel以外の層で非同期処理したい時 | Business Logicレイヤーでの自由度が高い |
おわりに
✅ Swift Concurrencyは読みやすさ・モダンな書き方が最高
✅ でもDispatchQueueは、制御・互換性・細かい設定がまだまだ現役
つまり、「どっちを使うか?」じゃなくて「いつ、どこで使い分けるか?」が大事ってこと。
SwiftUIのViewでは .task
や @MainActor
を使ってスッキリ書きたいけど、
ViewModelやModel層、レガシーとの橋渡しには DispatchQueue がシンプルに最適解。