3年ぐらい前の被埃記事なのにだいぶご覧いただいていて…なんかもういたたまれなくなって記法など内容更新しました。ちなみに、何だかんだ普段使うのって
DispatchQueue.main.async{ print("サブスレッドでUI処理") }
だけだったなあっていう雑感。。(2018.12.25)
知識整理を兼ねてざっとまとめます。Operationとかとの兼ね合いについてはSwift 並列プログラミング基礎で。
1. はじめに
GCDとは
- Grand Central Dispatch(直訳:総合送信?)
- マルチタスク実行基盤API
- もともとCベースで使うAPIだったが、今はSwiftライブラリDispatchが整備されている
- Operationはこれのラッパークラス
用法・効能
- 用意したディスパッチキューにタスクを放り込んで何やかんや。
- スレッド管理しなくても非同期処理を実現できる。
- 「直列処理と並列処理」「非同期と同期」といった組合せに対する専用APIも用意されており、複雑なタスク処理プロセスでも簡潔に記述できる。
- 使いこなせれば記述の短いきれいなコードに。
2. 使う前に ディスパッチキューについて
- Dispatch Queue(直訳:送信待ち行列?)
- 処理待ちタスクを追加するためのキュー。追加された順にタスクを処理側へ渡す役割を担う。タスクの処理は担当しない。
- ディスパッチキューが処理側へタスクを引き渡す方式は2種類。
- 直列処理(=SERIAL)…前タスク処理が終わるのを待って次タスクを処理側へ渡す方式。常に1つずつ順番通り処理されるので、マルチコアCPUの良さを発揮できないが、着実に処理される。順番が重要なタスクはこちらを選択すべき。(デフォルト)
- 並列処理(=CONCURRENT)…前タスク処理が終わっていなくともCPUに余力があれば次タスクを処理側へ渡す方式。処理が分散されるので、個々のタスク完了の順番が入れ替わることがあるが、マルチコアCPUの性能を発揮して全体的に速く処理される。順番を気にしないタスクはこちらを選択すべき。
- ディスパッチキュー優先度は、用途に合わせて5種類。他のディスパッチキューと比べた時の処理優先度となる。ディスパッチキューを自作する場合は微調整も可能。QoS(Quality of Service;サービス品質)と呼ばれる。
DispatchQoS.QoSClass.~
で指定する。
- USER_INTERACTIVE用…ユーザーからの入力をもとに即座に反映させる必要がある動作用。
- USER_INITIATE用…ユーザーからの入力をもとに反映させる一般動作用。
- DEFAULT用…直接設定するのは非推奨っぽい。標準。
- UTILITY用…プログレスバー動作など反映に緊急度が求められていない動作。
- BACKGROUND用… バックグラウンド動作用。
3. 基本の使い方① ディスパッチキューの用意
- 状況に応じたディスパッチキューを用意。
- メインディスパッチキュー
- グローバルディスパッチキュー
- プライベートディスパッチキュー
メインディスパッチキュー
- OS側で作成済みなので呼び出すだけ。
- 1つだけ存在。
- 直列処理タイプ。
- UI表示系タスクはここで行わないと動かない。
メインディスパッチキュー呼出し例
let mainQueue = DispatchQueue.main
グローバルディスパッチキュー
- OS側で作成済みなので呼び出すだけ。
- 5つ存在。(ただし、実質使えるのは4つ)
- 並列処理タイプ。
- 用途を指定して呼出し。
グローバルディスパッチキュー呼出し例
// UserInteractiveタイプ
let grobalQueue = DispatchQueue.global(qos: .userInteractive)
プライベートディスパッチキュー
- OS用意済みディスパッチキューだと不都合がある場合に作成。
- 処理規則や用途などを指定して作成。
プライベートディスパッチキュー作成例1
// 並列処理タイプ
// UserInteractive同等のディスパッチキュー優先度
let myQueue1 = DispatchQueue(label: "キュー1", qos: .userInteractive, attributes: .concurrent)
プライベートディスパッチキュー作成例2
// 並列処理タイプ
// UserInteractiveから少し落ちるディスパッチキュー優先度
let myQueue2 = DispatchQueue(label: "キュー2", qos: .userInitiated, attributes: .concurrent)
プライベートディスパッチキュー作成例3
// 並列処理タイプ
// ディスパッチキュー優先度を指定しない
let myQueue3 = DispatchQueue(label: "キュー3", attributes: .concurrent)
プライベートディスパッチキュー作成例4
// 直列処理タイプ
// ディスパッチキュー優先度を指定しない
let myQueue4 = DispatchQueue(label: "キュー4")
4. 基本の使い方② タスクの追加
タスク追加(sync、async)
- タスクを追加する。
- タスクをディスパッチキューに追加すると、基本的にすぐ処理側へ渡される。処理のタイミングを図りたい場合は、後述の遅延追加や一時停止を行う。
- タスク呼び出し元がタスクの処理完了を待つかどうかで2種類のタスク追加方法がある。
- 同期的:タスクをディスパッチキューに追加したあと、そのタスクの処理完了まで次の行に移行しない。(つまり、通常の記述と同じ)
- 非同期的:タスクをディスパッチキューに追加したら、そのタスクの処理完了を待たずに次の行に移行する。
タスクの同期的追加
dispatchQueue.sync {
print("同期")
}
タスクの非同期的追加
dispatchQueue.async {
print("非同期")
}
5. 特殊な使い方① タスクの特殊な追加方法
直列処理タスクの追加(barrier)
- .barrier設定とともに追加されたタスクについては、1つずつ順番に処理を行う。
- 直列処理ディスパッチキューでやっていることと同じなので、直列処理ディスパッチキューでやっても無意味。
- 並列処理ディスパッチキューの中で例外的に直列処理したいタスク群が存在する場合に使用。
直列処理タスクの追加
concurrentDispatchQueue.async(flags: .barrier) {
print("one!!")
}
concurrentDispatchQueue.async(flags: .barrier) {
print("two!!")
}
concurrentDispatchQueue.async(flags: .barrier) {
print("three!!")
}
タスクの遅延追加(asyncAfter)
- 設定時間経過後にタスクが追加される。
- 呼び出し元で遅延等は発生しない。(つまり、非同期的に追加される)
タスクの遅延追加
dispatchQueue.asyncAfter(deadline: .now() + 10) {
print("10秒後に実行")
}
6. 特殊な使い方② ディスパッチキューの操作
ディスパッチキューの一時停止(suspend)
- ディスパッチキューが処理側にタスクを渡すのを一時停止する。
- ただし、あくまで一時停止するのは処理側に渡すまでなので、すでにディスパッチキューから離れて処理が始まっているタスクは一時停止の範囲外。
ディスパッチキューの一時停止
// 一時停止
dispatchQueue.suspend()
// 再開
dispatchQueue.resume()
ディスパッチキューの完了状態の把握(DispatchGroup)
- 単体もしくは複数のキューに追加されたタスクの完了状況をまとめて把握したいときに用いる。
- 非同期処理の処理完了後に何かしたいときなど便利。
ディスパッチキューの完了状態の把握
//============= ディスパッチグループおよびディスパッチキューの作成 ============= //
let dispatchGroup = DispatchGroup()
let queue1 = DispatchQueue(label: "キュー1")
let queue2 = DispatchQueue(label: "キュー2", attributes: .concurrent)
//============= 単純なタスクの追加 ============= //
queue1.async(group: dispatchGroup) {
print("タスク1-1")
}
queue1.async(group: dispatchGroup) {
print("タスク1-2")
}
queue2.async(group: dispatchGroup) {
print("タスク2-1")
}
//============= 前の全タスクが全て処理し終わったあとに処理されるタスクの追加 ============= //
dispatchGroup.notify(queue: queue2) {
print("タスク2-2")
}
//============= グループ内の全タスク完了まで待機(※普通は別のとこで呼び出す) ============= //
// タイムアウト秒数を指定して待機。(タイムアウトさせない場合は、`.distantFuture`を指定)
dispatchGroup.wait(timeout: .now() + 10)
7. 特殊な使い方③ 特殊なディスパッチキュー
カウンタ管理(DispatchSemaphore)
- カウンタを管理するための特殊なディスパッチキュー。ObjectPoolパターンなど有限リソースの個数を管理する場面で使用。
-
signal()
でカウントアップ。 -
wait()
でカウントダウン。ただし、カウントが0以下(つまりカウントダウン不可)のときは1以上になるまでそこで待機。
①セマフォキューを作成
// リソース数を指定してセマフォキューを作成
let semaphoreQueue = DispatchSemaphore(value: 100)
②有限リソースを取り出す
//============= 記述例1 タイムアウトさせない ============= //
semaphoreQueue.wait()
print("リソースをオブジェクトプールから取り出す処理")
//============= 記述例2 タイムアウト秒数を指定 ============= //
// タイムアウト時間は10秒間
switch semaphoreQueue.wait(timeout: .now() + 10) {
case .success:
print("リソースをオブジェクトプールから取り出す処理")
case .timedOut:
print("タイムアウトしてリソースアクセスに失敗したことに対する処理")
}
③有限リソースを戻す
// リソースを戻す
- リソースをオブジェクトプールに戻す処理 -
// カウンタをインクリメント
semaphoreQueue.signal()
備考
WWDC2016のときの発表も取っ掛かりとしてはだいぶ良さげ。
Concurrent Programming With GCD in Swift 3