ちょっと話題の記事

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

2016.10.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは!モバイルアプリサービス部の加藤潤です。
今回はSwift3で大きく変わったGCD(Grand Central Dispatch)のAPIについて、その基本的な使い方を見ていきたいと思います。

基礎知識のおさらい

GCDの具体的な使い方に入る前に基礎知識のおさらいをしておきます。

同期と非同期

まずは同期と非同期についてです。
GCDではキュー(queue)という「処理の入れ物」に対して処理を登録することで、適切なスレッド上で処理が行われるのですが、キューに登録する際に以下の実行方式のいずれかを指定することができます。

  • 同期
    • キューに処理を登録したスレッドが登録した処理が完了するのを「待つ」
    • GCDのメソッドはsync
  • 非同期
    • キューに処理を登録したスレッドが登録した処理が完了するのを「待たない」
    • GCDのメソッドはasync

上記のように、違いは処理が完了するのを待つか待たないかです。

※本記事中のコード例は全てasyncにしています。 syncは使い方を間違えるとスレッドのデッドロックを起こす可能性があります。どうしてもsyncにする必要がない限り、基本的にasyncを使うことをオススメします。

キューにも種類がある

キューにも以下の種類があります。

  • Serial
    • 処理を直列に実行する。キュー内の前の処理が完了してから次の処理が開始する。一度に実行される処理は一つのみ。
  • Concurrent
    • 処理を並列に実行する。処理がキューから取り出される順番はFIFOだが、一度に複数の処理が実行され、処理が完了する順番もバラバラ。

特別なキューであるメインキュー

キューの中でもアプリ起動時にシステムによって自動的に作られるメインキュー(main queue)と呼ばれる特別なキューがあります。
このキューに登録した処理はメインスレッド上で直列に実行されます。特別なSerialキューであるとも言えます。

サブスレッドで何か重い処理をして終わったらメインスレッドに戻す

さて、ここからGCDの具体的な使い方を見ていきましょう。

iOSアプリでは負荷の高い処理やいつ終わるかわからない通信等の処理は基本的にメインスレッドでは行いません。 メインスレッドで行うとアプリが固まりユーザーの操作を妨げてしまうためです。 そこでよくあるパターンは、サブスレッドで何か重い処理をして終わったらメインスレッドに戻すというパターンです。

自分でキューを生成

以下は自分でキューを生成して任意の処理をサブスレッドで実行、終わったらメインスレッドで処理を実行する例です。

// キューを生成してサブスレッドで実行
DispatchQueue(label: "jp.classmethod.app.queue").async {
    self.doSomething()

    // メインスレッドで実行
    DispatchQueue.main.async {
        self.doSomething()
    }
}

ラベル名を指定してキューを生成しています。この時生成されるのは「Serial」キューです。
Concurrentキューを生成したい場合は以下のように.concurrentオプションを指定します。

DispatchQueue(label: "jp.classmethod.app.queue", attributes: .concurrent)

システムで用意されたキューを使う

上記で自分でキューを生成と書いた理由は以下のようにシステムで用意されたキューを使うことも可能だからです。

// システムで用意されたキューを使う
DispatchQueue.global(qos: .default).async {
    self.doSomething()
}

DispatchQueue.globalで取得できるキューはシステムで用意された「Concurrent」キューです。
引数で指定しているqosQuality of Serviceの略で、処理の優先度を決定するものです。ここではdefaultを指定していますが、優先度の高いものから順に以下の選択肢が用意されています。

  • userInteractive
  • userInitiated
  • default
  • utility
  • background
  • unspecified

指定した秒数後に処理を実行する

以下は5秒後に任意の処理を実行する例です。 DispatchTime.now()で取得した現在時間に対し、.seconds(5)で5秒足しています。secondsDispatchTimeIntervalというenumであり、他にmilliseconds(ミリ秒)、microseconds(マイクロ秒)、nanoseconds(ナノ秒)を指定することができます。

// 5秒後に実行
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(5)) {
    self.doSomething()
}

全てのキューの処理が終わったら任意の処理を行う

以下はキューが複数ある場合に、全てのキューの処理が終わったら任意の処理を行う例です。 最初にグループとキューを生成し、キューに処理を登録する際にグループを指定することでキューとグループを紐付けています。 全てのキューの処理が終わったらメインスレッド上で処理が行われます。

// グループを生成
let group = DispatchGroup()
let queue1 = DispatchQueue(label: "jp.classmethod.app.queue1")
let queue2 = DispatchQueue(label: "jp.classmethod.app.queue2")
let queue3 = DispatchQueue(label: "jp.classmethod.app.queue3")

// キューとグループを紐付ける
queue1.async(group: group) {
    print("queue1 done.")
}
queue2.async(group: group) {
    print("queue2 done.")
}
queue3.async(group: group) {
    print("queue3 done.")
}

// タスクが全て完了したらメインスレッド上で処理を実行する
group.notify(queue: DispatchQueue.main) {
    print("all task done.")
}

DispatchWorkItemを使って処理を行う

DispatchWorkItemはタスクをカプセル化したクラスです。このクラスを使うと以下のようにキューの指定が必須ではなくなります。 この場合は現在のコンテキスト(スレッド)上で処理が行われるようです。

// キューを指定しなくても処理が行える
let workItem = DispatchWorkItem {
    self.doSomething()
}
workItem.perform()

もちろん、以下のようにキューを指定することも可能です。

queue1.async(execute: workItem)

このDispatchWorkItemを使うと、処理の実行perform()の他にも待機wait()やキャンセルcancel()など、タスクに関しての制御を細かく行うことができます。 また、DispatchWorkItemFlags というオプションも用意されていて、QoSの細かい挙動等を調整することができます。

dispatch_onceが使えなくなった

Objective-Cの時からシングルトンパターンを実現するために使われていたdispatch_onceはSwift 3で使えなくなりました。 パラメーターであるdispatch_once_tも使えなくなっており、実際に以下のように使おうとしたところ、 'dispatch_once_t' is unavailable in Swift: Use lazily initialized globals insteadとエラーになりました。

let token: dispatch_once_t // 'dispatch_once_t' is unavailable in Swift: Use lazily initialized globals instead
dispatch_once(token) {
}

dispatch_onceがSwift 3で使えないことは、Migrating to Swift 2.3 or Swift 3 from Swift 2.2においても述べられています。

Swift 3でシングルトンパターンを実現するには

dispatch_onceが使えなくなったSwift 3ではシングルトンパターンを以下のようにstatic letプロパティで実現します。
この書き方自体は以前から使えており、Swift 3から使えるようになったわけではありません。 あくまでdispatch_onceが使えなくなったということです。
finalで継承を禁止し、さらにイニシャライザーをprivateにすることで自由なインスタンス生成を禁止し、static letとして公開したプロパティを通じてのみ唯一のインスタンスにアクセスできるようにします。

final class ClassA {

    static let sharedInstance = ClassA()

    private init() {

    }
}

おわりに

今回はSwift3におけるGCDの基本的な使い方について書いてみました。GCDは非常に奥が深い世界なので一朝一夕に全てを理解することは難しいかもしれませんが、基本的なことから学んでいきたいと思います。

参考