Edited at

【Swift3】GCDについて勉強してみた

More than 1 year has passed since last update.

フレームワーク等を使わずに、Swift3の標準ライブラリで非同期処理や排他処理を実現する


直列処理

時間のかかる処理をすべてメインスレッド上で直列に実行する

非常に時間がかかる


ViewController.swift

// 時間のかかる処理

func doSomething(number: Int) {
print("Task \(number): Start")
for i in 0 ... 10 {
sleep(1)
print("Task \(number): Running ... \(i * 10)%")
}
print("Task \(number): Completed")
}

// すべて直列で処理
func inMainQueueOnly() {
for i in 1 ... 3 {
doSomething(number: i)
}
print("All tasks completed.")
}


出力結果は以下の通り

Task 1: Start

Task 1: Running ... 0%
Task 1: Running ... 10%
(中略)
Task 1: Running ... 90%
Task 1: Running ... 100%
Task 1: Completed
Task 2: Start
Task 2: Running ... 0%
Task 2: Running ... 10%
(中略)
Task 2: Running ... 90%
Task 2: Running ... 100%
Task 2: Completed
Task 3: Start
Task 3: Running ... 0%
Task 3: Running ... 10%
(中略)
Task 3: Running ... 90%
Task 3: Running ... 100%
Task 3: Completed
All tasks completed.


キューを並列で処理

時間のかかる処理を非同期で処理する

DispatchQueueを使うことで、キューの処理を並列で行うことができる

DispatchQueueの細かい使い方などは、後述の参考サイトで説明されているので、そちらを参照


ViewController.swift

// キューを並列で処理

func inConcurrentQueue() {
for i in 1 ... 3 {
DispatchQueue.global(qos: .default).async {
self.doSomething(number: i)
}
}
print("ここはキューの処理完了を待たずに実行される")
}

出力結果は以下の通り

ブロック外のprint文がキューの処理を待たずに実行されているのがわかる

ここはキューの処理完了を待たずに実行される

Task 1: Start
Task 2: Start
Task 3: Start
Task 2: Running ... 0%
Task 1: Running ... 0%
Task 3: Running ... 0%
Task 1: Running ... 10%
Task 2: Running ... 10%
Task 3: Running ... 10%
Task 1: Running ... 20%
Task 2: Running ... 20%
Task 3: Running ... 20%
Task 1: Running ... 30%
Task 2: Running ... 30%
(中略)
Task 2: Running ... 80%
Task 3: Running ... 80%
Task 1: Running ... 80%
Task 2: Running ... 90%
Task 3: Running ... 90%
Task 1: Running ... 90%
Task 2: Running ... 100%
Task 3: Running ... 100%
Task 1: Running ... 100%
Task 2: Completed
Task 3: Completed
Task 1: Completed


複数のキューの並列処理終了後に処理を行う

例えば、Twitterで複数の画像を送信するAPIは、


  1. 画像のuploadリクエスト→レスポンスからmediaIDを得る

  2. 画像の枚数分上の処理を行い、media_idsというパラメータを作成する

  3. media_idsをパラメータにもつツイートを送信する

という手順で処理されるが、1のリクエストを最大4枚分、直列で行うと非常に時間がかかる

そのため、画像送信リクエストは並列で処理を行い、レスポンスを得られたものから順にmedia_idsにmediaIDを追記し、すべての画像送信リクエストからレスポンスを得たら、3の処理を行う

これを実現するための方法は色々あるようだが、いちばん簡単なのはDispatchGroupを使うことと思われる


ViewController.swift

// 複数のキューを並列処理した後に、さらに処理を行う

func doMoreAfterConcurretQueuesUsingDispatchGroup() {
let group = DispatchGroup()

for i in 1 ... 3 {
group.enter()
DispatchQueue.global(qos: .default).async {
self.doSomething(number: i)
group.leave()
}
}
group.notify(queue: DispatchQueue.global(qos: .default)) {
print("すべてのキューの処理が完了しました")
}
}


出力結果は以下の通り

Task 1: Start

Task 2: Start
Task 3: Start
Task 2: Running ... 0%
Task 1: Running ... 0%
Task 3: Running ... 0%
Task 3: Running ... 10%
Task 1: Running ... 10%
Task 2: Running ... 10%
Task 3: Running ... 20%
(中略)
Task 3: Running ... 80%
Task 1: Running ... 90%
Task 2: Running ... 90%
Task 3: Running ... 90%
Task 1: Running ... 100%
Task 2: Running ... 100%
Task 3: Running ... 100%
Task 1: Completed
Task 2: Completed
Task 3: Completed
すべてのキューの処理が完了しました


何か処理した後に値を返す

非同期処理は、クロージャを使用することが多いが、クロージャが深くネストしていくことがよくある

そこで、クロージャの連鎖を断ち切るために、メソッドの返り値として非同期処理の結果を得たいことがあると思う

しかし、クロージャ内でreturnをしても(当たり前だが)クロージャから抜け出すだけである

また、クロージャ外でreturnをすると、クロージャの処理を待たずにreturnすることになる

先ほどのDispatchGroupを使ったとしても、以下の例のように構文エラーとなり、適切にreturnできない


ViewController.swift

// 並列処理した後に値を返す

// このように書きたいところだが、これは構文として正しくない
func returnsValueAfterConcurretQueuesUsingDispatchGroup() -> String {
var string = "初期値"
let group = DispatchGroup()

for i in 1 ... 3 {
group.enter()
DispatchQueue.global(qos: .default).async {
self.doSomething(number: i)
string += " => Task\(i)が終了"
group.leave()
}
}
group.notify(queue: DispatchQueue.global(qos: .default)) {
return string // 警告 ==> Expression of type "String" is unused
}
} // エラー ==> Missing return in a function expected to return "String"


そこで、DispatchSemaphoreを使う

DispatchSemaphoreは、イニシャライザでvalueを設定し


  1. signal()が呼ばれるとvalueがインクリメントされる

  2. wait()はvalueが0または負の間処理がそこでストップする

  3. wait()はvalueが正になると進み、valueをデクリメントする

という仕組みになっている

この仕組みを使うと、returnをブロック内に書く必要がなくなり、めでたく適切なタイミングでreturnすることができるようになる


ViewController.swift

// 何か処理した後に値を返す(セマフォを使用)

func returnsValueAfterAQueueUsingSemaphore() -> String {
var string = "初期値"
let semaphore = DispatchSemaphore(value: 0)

DispatchQueue.global(qos: .default).async {
self.doSomething(number: 1)
string += " => Task1が終了"
semaphore.signal()
}

semaphore.wait()
return string
}


出力結果は以下の通り

返り値にキュー内の処理の結果が反映されている

Task 1: Start

Task 1: Running ... 0%
Task 1: Running ... 10%
Task 1: Running ... 20%
Task 1: Running ... 30%
Task 1: Running ... 40%
Task 1: Running ... 50%
Task 1: Running ... 60%
Task 1: Running ... 70%
Task 1: Running ... 80%
Task 1: Running ... 90%
Task 1: Running ... 100%
Task 1: Completed
初期値 => Task1が終了


複数のキューの並列処理後に値を返す

先のTwitterの例のように、非同期処理を行い、それが終了したあとで更に処理を行うという場合も、DispatchSemaphoreを使うことで返り値を返すことができる

この場合は、非同期処理中のsemaphore.signal()が呼び出された回数分、semaphore.wait()を呼び出し、セマフォのvalueが正になるタイミングを調整している

// キューを並列処理した後に値を返す(セマフォを使用)

func returnsValueAfterConcurrentQueuesUsingSemaphore() -> String {
var string = "初期値"
let semaphore = DispatchSemaphore(value: 0)

for i in 1 ... 3 {
DispatchQueue.global(qos: .default).async {
self.doSomething(number: i)
string += " => Task\(i)が終了"
semaphore.signal()
}
}

for _ in 1 ... 3 {
semaphore.wait()
}
return string
}

出力結果は以下の通り

Task 1: Start

Task 2: Start
Task 3: Start
Task 1: Running ... 0%
Task 2: Running ... 0%
Task 3: Running ... 0%
Task 1: Running ... 10%
Task 2: Running ... 10%
(中略)
Task 3: Running ... 80%
Task 1: Running ... 90%
Task 2: Running ... 90%
Task 3: Running ... 90%
Task 1: Running ... 100%
Task 1: Completed
Task 3: Running ... 100%
Task 3: Completed
Task 2: Running ... 100%
Task 2: Completed
初期値 => Task3が終了 => Task1が終了 => Task2が終了


参考サイト

GCD のディスパッチセマフォを活用する (Objective-C〜Swift 3 対応)

Swift 世代の排他制御

【iOS Swift入門 #170】複数の非同期処理を制御する ~GCD(Grand Central Dispatch)~

[Swift 3] Swift 3時代のGCDの基本的な使い方

Swiftでの並列プログラミングについて調べてみた。(その1)

並列プログラミングガイド


ソース

今回の全ソースはこちら