【Xcode/SwiftUI】今更だけど、DispatchQueueを復習してみる

はじめに

なぜ今、DispatchQueueを復習するのか

「Swift Concurrency(async/await)出たし、もうDispatchQueue不要?」
と思うかもしれないけど、実務だと全然通用しない場面あるので改めて復習しておくとGoodだぞという試み

たしかに async/await はキレイでモダン。でも、

  • 既存コードは DispatchQueue ゴリゴリ
  • SwiftUIでも .task {} の中で使うこと多い
  • 低レベルな制御(QoSとか、キャンセル、通知)したい時は未だに便利
  • 面接で「メインスレッドってなに?」って聞かれる

こういう現場・面接・デバッグの瞬間に、頭の片隅に GCD(Grand Central Dispatch)が入ってるかで差が出る。

Swift Concurrencyとの使い分けも踏まえて

今は「使い分けの時代」。
どっちか片方だけで完結するプロジェクトって、少ないかも。

やりたいことSwift ConcurrencyDispatchQueue
複雑な並列処理△(できるけど見にくくなる)
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 がシンプルに最適解。