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

Grand Central Dispatch (GCD) は Mac OS X 10.6+ や iOS 4+ で利用出来る技術で, タスクを簡単に非同期で実行させることが出来る技術の一つです.

この記事では GCD の機能の一つであるディスパッチセマフォを活用する方法を紹介します. 例として有限リソースへのアクセス制限と, タスクの実行待ちを取り上げます.

この説明では Swift 3 を利用しています. Swift 3 から API の命名規則が大きく変わっていますが, 同じ機能は Swift 3 未満や Objective-C でも利用できます. Objective-C や Swift 1/2 での記法はこの記事の末尾を参照してください.

ディスパッチセマフォとは

基本的にカウンティングセマフォと同じですが, より効率的に動作するように実装されていると公式ドキュメントに記述されています.

従来からあるセマフォと似ていますが、より効率的に動作します。カーネル機能を呼び出すのは、セマフォが使えないため、呼び出し元のスレッドをブロックしなければならない場合だけだからです。セマフォが使えれば、カーネル呼び出しは起こりません。
Apple, Inc. 並列プログラミングガイド p.40

基本的な操作として create, wait, signal の3つがあります.

  • DispatchSemaphore(value: value) — 初期値を指定してセマフォを作成する
  • semaphore.wait() — セマフォの値をデクリメントする (P操作)
  • semaphore.signal() — セマフォの値をインクリメントする (V操作)

有限のリソースへのアクセスを制限する

セマフォの活用方法として数が有限のリソースに対して, 同時にアクセスできる数を制限するというものがあります. (例: 同時に開けるファイルの数, 通信における同時接続数)

まずセマフォを使わない場合の例として, 以下のコードを挙げます. このプログラムではログメッセージを表示しながら3秒間待つというタスクを, グローバルな並列ディスパッチキューを用いて, 10個並列で実行します.

func example1() {
    let queue = DispatchQueue.global(qos: .default)
    for i in 0..<10 {
        queue.async {
            NSLog("Start: \(i)")
            print("Start: \(i)")
            sleep(3)
            NSLog("End: \(i)")
        }
    }
}
example1()

実行例は以下のようになります. 並列キューを利用しているためタスクの完了を待たずに10個のタスクが並列で実行され, 3秒後にタスクが終了しています.

2016-10-28 23:19:39.595 SemaphoreTest[19679:5314281] Start: 0
2016-10-28 23:19:39.595 SemaphoreTest[19679:5314291] Start: 7
2016-10-28 23:19:39.595 SemaphoreTest[19679:5314289] Start: 5
2016-10-28 23:19:39.595 SemaphoreTest[19679:5314284] Start: 2
2016-10-28 23:19:39.595 SemaphoreTest[19679:5314292] Start: 9
2016-10-28 23:19:39.595 SemaphoreTest[19679:5314290] Start: 6
2016-10-28 23:19:39.595 SemaphoreTest[19679:5314293] Start: 8
2016-10-28 23:19:39.595 SemaphoreTest[19679:5314288] Start: 4
2016-10-28 23:19:39.595 SemaphoreTest[19679:5314287] Start: 3
2016-10-28 23:19:39.595 SemaphoreTest[19679:5314282] Start: 1
2016-10-28 23:19:42.595 SemaphoreTest[19679:5314291] End: 7
2016-10-28 23:19:42.595 SemaphoreTest[19679:5314281] End: 0
2016-10-28 23:19:42.595 SemaphoreTest[19679:5314289] End: 5
2016-10-28 23:19:42.595 SemaphoreTest[19679:5314284] End: 2
2016-10-28 23:19:42.596 SemaphoreTest[19679:5314292] End: 9
2016-10-28 23:19:42.596 SemaphoreTest[19679:5314290] End: 6
2016-10-28 23:19:42.596 SemaphoreTest[19679:5314293] End: 8
2016-10-28 23:19:42.596 SemaphoreTest[19679:5314288] End: 4
2016-10-28 23:19:42.596 SemaphoreTest[19679:5314287] End: 3
2016-10-28 23:19:42.597 SemaphoreTest[19679:5314282] End: 1

次にセマフォを使って, 同時に2個のタスクしか実行できないようにした例です. NSLog("Start: \(i)") から NSLog("End: \(i)") の部分は同時に2個のタスクでしか実行されないことを保証できます.

func semaphoreExample1() {
    let semaphore = DispatchSemaphore(value: 2)
    let queue = DispatchQueue.global(qos: .default)
    for i in 0..<10 {
        queue.async {
            semaphore.wait()
            NSLog("Start: \(i)")
            sleep(3)
            NSLog("End: \(i)")
            semaphore.signal()
        }
    }
}
semaphoreExample1()

実行例は以下の通りです. 時間を見れば分かる通り, NSLog("Start: \(i)") から NSLog("End: \(i)") の部分が同時に2個しか実行されていないことを確認できます.

2016-10-28 23:24:15.270 SemaphoreTest[19892:5321690] Start: 0
2016-10-28 23:24:15.270 SemaphoreTest[19892:5321704] Start: 1
2016-10-28 23:24:18.342 SemaphoreTest[19892:5321690] End: 0
2016-10-28 23:24:18.342 SemaphoreTest[19892:5321704] End: 1
2016-10-28 23:24:18.343 SemaphoreTest[19892:5321693] Start: 2
2016-10-28 23:24:18.343 SemaphoreTest[19892:5321691] Start: 3
2016-10-28 23:24:21.370 SemaphoreTest[19892:5321693] End: 2
2016-10-28 23:24:21.370 SemaphoreTest[19892:5321691] End: 3
2016-10-28 23:24:21.370 SemaphoreTest[19892:5321713] Start: 5
2016-10-28 23:24:21.371 SemaphoreTest[19892:5321712] Start: 4
2016-10-28 23:24:24.371 SemaphoreTest[19892:5321713] End: 5
2016-10-28 23:24:24.371 SemaphoreTest[19892:5321712] End: 4
2016-10-28 23:24:24.371 SemaphoreTest[19892:5321715] Start: 7
2016-10-28 23:24:24.371 SemaphoreTest[19892:5321714] Start: 6
2016-10-28 23:24:27.390 SemaphoreTest[19892:5321715] End: 7
2016-10-28 23:24:27.390 SemaphoreTest[19892:5321714] End: 6
2016-10-28 23:24:27.406 SemaphoreTest[19892:5321716] Start: 8
2016-10-28 23:24:27.406 SemaphoreTest[19892:5321717] Start: 9
2016-10-28 23:24:30.443 SemaphoreTest[19892:5321717] End: 9
2016-10-28 23:24:30.443 SemaphoreTest[19892:5321716] End: 8

タスクの完了を待機する

初期値 0 のセマフォを利用することで, タスクの処理の完了を待つという使い方ができます. プログラムと実行例は以下の通りです.

func semaphoreExample2() {
    let semaphore = DispatchSemaphore(value: 0)
    let queue = DispatchQueue.global(qos: .default)
    queue.async {
        NSLog("Running async task...")
        sleep(3)
        NSLog("Async task completed")
        // セマフォの値をインクリメントする, つまりセマフォの値が正になる
        semaphore.signal()
    }
    NSLog("Waiting async task...")
    // セマフォの値が正になるのを待つ
    // セマフォの値が正になったら, デクリメントして進む
    semaphore.wait()
    NSLog("Continue!")
}
semaphoreExample2()
2016-10-29 11:56:14.884 SemaphoreTest[21955:5357365] Waiting async task...
2016-10-29 11:56:14.884 SemaphoreTest[21955:5357413] Running async task...
2016-10-29 11:56:17.887 SemaphoreTest[21955:5357413] Async task completed
2016-10-29 11:56:17.887 SemaphoreTest[21955:5357365] Continue!

以上の実行結果からも, 非同期に実行したタスクが完了するのを待って, 処理を継続することが出来ていることがわかります.

複数のタスクの完了を待機する

上記の1つのタスクの完了を待機する例を拡張すると, 複数のタスクの完了を待機するプログラムもセマフォを使って書くことができます. それぞれのタスクで semaphore.signal() を呼び出し, その数に対応するだけ semaphore.wait() を呼び出します.

func semaphoreExample3() {
    let semaphore = DispatchSemaphore(value: 0)
    let queue = DispatchQueue.global(qos: .default)
    let n = 9
    for i in 0..<n {
        queue.async {
            NSLog("\(i): Running async task...")
            sleep(3)
            NSLog("\(i): Async task completed")
            semaphore.signal()
        }
    }
    NSLog("Waiting async task...")
    for i in 0..<n {
        semaphore.wait()
        NSLog("\(i + 1)/\(n) completed")
    }
    NSLog("Continue!")
}
semaphoreExample3()
2016-10-29 11:58:17.624 SemaphoreTest[22067:5360551] 1: Running async task...
2016-10-29 11:58:17.624 SemaphoreTest[22067:5360586] 3: Running async task...
2016-10-29 11:58:17.624 SemaphoreTest[22067:5360548] 2: Running async task...
2016-10-29 11:58:17.624 SemaphoreTest[22067:5360549] 0: Running async task...
2016-10-29 11:58:17.625 SemaphoreTest[22067:5360587] 4: Running async task...
2016-10-29 11:58:17.625 SemaphoreTest[22067:5360588] 5: Running async task...
2016-10-29 11:58:17.625 SemaphoreTest[22067:5360589] 6: Running async task...
2016-10-29 11:58:17.626 SemaphoreTest[22067:5360590] 7: Running async task...
2016-10-29 11:58:17.624 SemaphoreTest[22067:5360214] Waiting async task...
2016-10-29 11:58:17.626 SemaphoreTest[22067:5360592] 8: Running async task...
2016-10-29 11:58:20.644 SemaphoreTest[22067:5360586] 3: Async task completed
2016-10-29 11:58:20.644 SemaphoreTest[22067:5360551] 1: Async task completed
2016-10-29 11:58:20.644 SemaphoreTest[22067:5360548] 2: Async task completed
2016-10-29 11:58:20.644 SemaphoreTest[22067:5360549] 0: Async task completed
2016-10-29 11:58:20.644 SemaphoreTest[22067:5360587] 4: Async task completed
2016-10-29 11:58:20.644 SemaphoreTest[22067:5360588] 5: Async task completed
2016-10-29 11:58:20.644 SemaphoreTest[22067:5360589] 6: Async task completed
2016-10-29 11:58:20.644 SemaphoreTest[22067:5360590] 7: Async task completed
2016-10-29 11:58:20.644 SemaphoreTest[22067:5360592] 8: Async task completed
2016-10-29 11:58:20.646 SemaphoreTest[22067:5360214] 1/9 completed
2016-10-29 11:58:20.648 SemaphoreTest[22067:5360214] 2/9 completed
2016-10-29 11:58:20.649 SemaphoreTest[22067:5360214] 3/9 completed
2016-10-29 11:58:20.650 SemaphoreTest[22067:5360214] 4/9 completed
2016-10-29 11:58:20.651 SemaphoreTest[22067:5360214] 5/9 completed
2016-10-29 11:58:20.651 SemaphoreTest[22067:5360214] 6/9 completed
2016-10-29 11:58:20.655 SemaphoreTest[22067:5360214] 7/9 completed
2016-10-29 11:58:20.662 SemaphoreTest[22067:5360214] 8/9 completed
2016-10-29 11:58:20.663 SemaphoreTest[22067:5360214] 9/9 completed
2016-10-29 11:58:20.664 SemaphoreTest[22067:5360214] Continue!

複数のタスクの完了を待機する (ディスパッチグループを利用する場合)

上記の例はディスパッチセマフォではなく, ディスパッチグループを利用しても実装することができます. 今回の例ではこの方法の方がよりシンプルかもしれません.

ディスパッチグループは一連のタスクをグループ化しておき, その全てのタスクの完了を待機させつことができます. プログラムと実行例は以下の通りです.

func semaphoreExample4() {
    let queue = DispatchQueue.global(qos: .default)
    let group = DispatchGroup()
    let n = 9
    for i in 0..<n {
        queue.async(group: group) {
            NSLog("\(i): Running async task...")
            sleep(3)
            NSLog("\(i): Async task completed")
        }
    }
    NSLog("Waiting async task...")
    group.wait()
    NSLog("Continue!")
}
semaphoreExample4()
2016-10-29 12:02:21.135 SemaphoreTest[22218:5365232] 1: Running async task...
2016-10-29 12:02:21.135 SemaphoreTest[22218:5365251] 0: Running async task...
2016-10-29 12:02:21.135 SemaphoreTest[22218:5365231] 3: Running async task...
2016-10-29 12:02:21.135 SemaphoreTest[22218:5365234] 2: Running async task...
2016-10-29 12:02:21.135 SemaphoreTest[22218:5365260] 4: Running async task...
2016-10-29 12:02:21.135 SemaphoreTest[22218:5365261] 5: Running async task...
2016-10-29 12:02:21.135 SemaphoreTest[22218:5365262] 6: Running async task...
2016-10-29 12:02:21.136 SemaphoreTest[22218:5365263] 7: Running async task...
2016-10-29 12:02:21.136 SemaphoreTest[22218:5365264] 8: Running async task...
2016-10-29 12:02:21.135 SemaphoreTest[22218:5365189] Waiting async task...
2016-10-29 12:02:24.135 SemaphoreTest[22218:5365232] 1: Async task completed
2016-10-29 12:02:24.135 SemaphoreTest[22218:5365251] 0: Async task completed
2016-10-29 12:02:24.136 SemaphoreTest[22218:5365231] 3: Async task completed
2016-10-29 12:02:24.136 SemaphoreTest[22218:5365234] 2: Async task completed
2016-10-29 12:02:24.137 SemaphoreTest[22218:5365260] 4: Async task completed
2016-10-29 12:02:24.137 SemaphoreTest[22218:5365261] 5: Async task completed
2016-10-29 12:02:24.137 SemaphoreTest[22218:5365262] 6: Async task completed
2016-10-29 12:02:24.138 SemaphoreTest[22218:5365264] 8: Async task completed
2016-10-29 12:02:24.138 SemaphoreTest[22218:5365263] 7: Async task completed
2016-10-29 12:02:24.139 SemaphoreTest[22218:5365189] Continue!

Swift 3 と Swift 1/2・Objective-C の対応

Swift 3

// Swift 3
let queue = DispatchQueue.global(qos: .default)
queue.async { print("Hello") }

let semaphore = DispatchSemaphore(value: 1)
semaphore.wait()
semaphore.signal()

let group = DispatchGroup()
queue.async(group: group) { print("Hi!") }
group.wait()

Swift 1/2

// Swift 1/2
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(queue) { print("Hello") }

let semaphore = dispatch_semaphore_create(1)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
dispatch_semaphore_signal(semaphore)

let group = dispatch_group_create()
dispatch_group_async(group, queue) { print("Hi!") }
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)

Objective-C

// Objective-C
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{ NSLog(@"Hello"); });

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(semaphore);

dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ NSLog(@"Hi!"); });
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

まとめ

macOS・iOS ではセマフォが GCD にディスパッチセマフォとして実装されており, 簡単に利用することができます. ディスパッチセマフォを使って有限リソースへのアクセス制限や, タスクの完了待ちなどを実装することができます. GCD にはセマフォよりも高級なインターフェイスも提供されているため, 適材適所で使い分ける必要があります.

参考

更新履歴

  • 2016/7/12 コードの表示崩れを修正
  • 2016/10/29 Objective-C・Swift 3 に対応
  • 2016/12/4 軽微な修正