32
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-08

フレームワーク等を使わずに、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)
並列プログラミングガイド

#ソース
今回の全ソースはこちら

32
28
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?