GCD (Grand Central Dispatch) - Dispatch Queue でマルチスレッドプログラミング

エキスパートObjective-Cプログラミングを読んで覚えたことのまとめ。 システムが管理するマルチスレッドの仕組み。 従来の Objective-C によるマルチスレッドよりも簡潔に記述でき、効率的な処理が可能となる。

Dispatch Queue の種類

登録した処理が同期的に実行される Serial Dispatch Queue と 登録した処理が並列実行される Concurrent Dispatch Queue の2種類がある。

Serial DIspatch Queue はスレッド1つを生成して実行される。 Concurrent Dispatch Queue はシステムが管理しているスレッドで実行される。

Dispatch Queuedispatch_queue_t 型であり, dispatch_queue_create 関数を使用して生成し、 dispatch_release 関数で破棄する。ただしiOS6からはARC管理化に置かれるので iOS5, iOS6 を共存させる場合は注意が必要。

//
// Serial Dispatch Queue (非ARC)
//

// Serial Dispath Queue を生成する場合は第2引数に
// NULL または DISPATCH_QUEUE_SERIAL を指定する。
// 第2引数は NULL でもいいけど各キューにユニークな名前を指定べき
dispatch_queue_t queue = dispatch_queue_create("jp.fernweh", NULL);
for (int i = 0; i < 5; i ++) {
  dispatch_async(queue, ^{ NSLog(@"%d", i); });
}
NSLog(@"###");

// createしたqueueはreleaseする必要がある
dispatch_release(queue);

結果

0
###
1
2
3
4
//
// Concurrent Dispatch Queue (非ARC)
//

// Concurrent Dispath Queue を生成する場合は第2引数に
// DISPATCH_QUEUE_CONCURRENT を指定
dispatch_queue_t queue = dispatch_queue_create("jp.fernweh",
                                               DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 5; i ++) {
  dispatch_async(queue, ^{ NSLog(@"%d", i); });
}
NSLog(@"###");

// createしたqueueはreleaseする必要がある
dispatch_release(queue);

結果

1
3
###
0
2
4

システム標準の Dispatch Queue

dispatch_get_*** でシステムが提供している標準の Dispatch Queue を取得できる。 これらはメモリ管理する必要は無いので dispatch_release などは使わなくていい。

※使っても何もおこらないらしい。

//
// (非ARC)
//

// メインスレッドで実行される Serial Dispatch Queue を取得
dispatch_queue_t mainQ = dispatch_get_main_queue();

// システムが標準で提供する Concurrent Dispatch Queue を取得
// dispatch_get_global_queue の第1匹数は優先度を HIGH ~ BACKGROUND で指定
// dispatch_get_global_queue の第2引数は何か知らないけど 0 を指定
dispatch_queue_t globalQ;
switch (DISPATCH_QUEUE_PRIORITY_HIGH) {
  case DISPATCH_QUEUE_PRIORITY_HIGH:
    globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    break;

  case DISPATCH_QUEUE_PRIORITY_DEFAULT:
    globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    break;

  case DISPATCH_QUEUE_PRIORITY_LOW:
    globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    break;

  case DISPATCH_QUEUE_PRIORITY_BACKGROUND:
    globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    break;

  default:
    break;
}

dispatch_set_target_queue で優先順位を指定したりキューを纏めたり

//
// (非ARC)
//

// 生成した Dispatch Queue
dispatch_queue_t q = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);

// 優先順位バックグラウンドの Dispatch Queue
dispatch_queue_t backgroundQ
  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

// 優先順位バックグラウンドの Dispatch Queue に追加
dispatch_set_target_queue(q, backgroundQ);

// なんか実行
dispatch_async(q, ^{
  // do something...
});

dispatch_release(q);
//
//  (非ARC)
//
dispatch_queue_t serialQ = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
for (NSInteger i = 0; i < 5; i++) {
  dispatch_queue_t q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);

  dispatch_set_target_queue(q, serialQ);
  dispatch_async(q, ^{ NSLog(@"%d", i); } );
  dispatch_release(q);
}

dispatch_release(serialQ);

数値が連番で出力される。

dispatch_after で一定時間後に実行

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
  // 約3秒後に実行される
  // ※ull は unsigned long long
  // 時刻の生成には dispatch_walltime 関数も利用可能
});

dispatch_group_*** グループで纏めて管理

//
// (非ARC)
//
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue
  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, globalQueue, ^{ NSLog(@"A"); });
dispatch_group_async(group, globalQueue, ^{ NSLog(@"B"); });
dispatch_group_async(group, globalQueue, ^{ NSLog(@"C"); });
dispatch_group_async(group, globalQueue, ^{ NSLog(@"D"); });
dispatch_group_async(group, globalQueue, ^{ NSLog(@"E"); });

// dispatch_group_notify により
// 上の5つの Dispatch Queue がすべて終了したときに
// Z が出力される。(A,B,C,D,E の出力順は不定)
dispatch_group_notify(group, globalQueue, ^ { NSLog(@"Z"); });

// dispatch_group_wait でグループの処理が終了するまで停止。
// 第2引数は dispath_time_t 型で DISPATCH_TIME_FOREVER
// の場合は処理が終わるまで現在のスレッドを止める。
NSLog(@"Will Wait");
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"Did Wait");

dispatch_release(group);

結果

Will Wait
B
A
C
D
E
Did Wait
Z

A, B, C, D, E の出力順は不定。ほかは固定。

//
// (非ARC)
//
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue
  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, globalQueue, ^{ NSLog(@"A"); });
dispatch_group_async(group, globalQueue, ^{ NSLog(@"B"); });
dispatch_group_async(group, globalQueue, ^{ NSLog(@"C"); });
dispatch_group_async(group, globalQueue, ^{ NSLog(@"D"); });
dispatch_group_async(group, globalQueue, ^{ NSLog(@"E"); });

// dispatch_group_wait は処理中のキューが存在しない場合は 0 を返す
long result;
do {
  // 処理中のキューが存在しない場合
  result = dispatch_group_wait(group, DISPATCH_TIME_NOW);
  NSLog(@"%ld", result);
} while (result != 0);

NSLog(@"ここはgroupの処理がすべて終了後に実行される");

dispatch_release(group);

結果

B
C
49
E
49
D
A
49
0
ここはgroupの処理がすべて終了後に実行される

※49が何かは知らない。

dispatch_barrier_async

以前の並列処理が終了するまで待ってくれる。

//
// (非ARC)
//

// dispatch_barrier_async は
// dispatch_queue_create で作成した Concurrent Dispatch Queue に対して使用する。
// dispatch_get_global_queue でやると…バリアしてくれんかった。。
dispatch_queue_t concurrentQ = dispatch_queue_create("Concurrent Queue",
                                                     DISPATCH_QUEUE_CONCURRENT);

dispatch_async(concurrentQ, ^{ NSLog(@"1"); });
dispatch_async(concurrentQ, ^{ NSLog(@"2"); });
dispatch_async(concurrentQ, ^{ NSLog(@"3"); });
dispatch_async(concurrentQ, ^{ NSLog(@"4"); });
dispatch_async(concurrentQ, ^{ NSLog(@"5"); });

dispatch_barrier_async(concurrentQ, ^{ NSLog(@"---"); });

dispatch_async(concurrentQ, ^{ NSLog(@"A"); });
dispatch_async(concurrentQ, ^{ NSLog(@"B"); });
dispatch_async(concurrentQ, ^{ NSLog(@"C"); });
dispatch_async(concurrentQ, ^{ NSLog(@"D"); });
dispatch_async(concurrentQ, ^{ NSLog(@"E"); });

dispatch_release(concurrentQ);

結果

2
4
1
3
5
---
A
B
C
D
E

※数字の部分とアルファベットの部分はそれぞれ必ず --- 前後に分かれて出力される。

dispatch_sync

ブロック処理を同期実行(実行が終わるまで待つ)。

デッドロックに注意。

dispatch_queue_t globalQ
  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

dispatch_async(globalQ, ^{ NSLog(@"A"); });
dispatch_async(globalQ, ^{ NSLog(@"B"); });
dispatch_async(globalQ, ^{ NSLog(@"C"); });
dispatch_async(globalQ, ^{ NSLog(@"D"); });
dispatch_async(globalQ, ^{ NSLog(@"E"); });
dispatch_async(globalQ, ^{ NSLog(@"F"); });

NSLog(@"1");
dispatch_sync(globalQ, ^{ NSLog(@"2"); });
NSLog(@"3");
dispatch_sync(globalQ, ^ { NSLog(@"4"); });
NSLog(@"5");

結果

A
1
C
2
3
B
4
D
5
E
F

アルファベット部分の出力順は不定だが、 数値部分は 1 ~ 5 で順番に出力される。

dispatch_apply 高速列挙的なもの

並列処理が終了するのを待つためデッドロックに注意。 また使用されるスレッド数に注意する必要がある。

//
// 簡単な使い方
//
dispatch_queue_t globalQ
  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSArray *array = @[@"A", @"B", @"C", @"D", @"E"];

dispatch_apply(array.count, globalQ, ^(size_t index) {
  NSLog(@"%@", array[index]);
});

NSLog(@"最後に出力される");

結果

A
C
B
E
D
最後に出力される

公式ドキュメントによると、dispatch_apply に渡すブロックの処理が軽い場合は普通にfor文使用した方が高速とのこと。

ハマった

以下のコードで、 "Start" が出力されてから "End" が出力されるまでに約何秒かかるかわかるだろうか?

dispatch_queue_t globalQ
  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSArray *array = @[@"A", @"B", @"C", @"D", @"E"];

NSLog(@"Start");
dispatch_apply(array.count, globalQ, ^(size_t index) {
  sleep(1); // 1秒スリープ
});
NSLog(@"End");

答えは約3秒。ワケがわからないよ。 ワケが分からなかったのでテキトーにコード書いて調べてみたら dispatch_apply は 2スレッドしか使用していない。

dispatch_apply を呼び出したスレッド と dispatch_apply の引数に指定した dispatch_queue で割り当てられたスレッド の2つ。 だから 要素数 5 / 2 = 2.5 …繰り上げて約3秒の時間がかかることになる。

※CPUのコア数によってかわるのかなと思ったけどこれを試したのは4コア

まあでも ASCII.jp:マルチコア時代の新機軸! Snow LeopardのGCD (4/4)|もっと知りたい! Snow Leopard を読むと速度は速いっぽい。

dispatch_suspend, dispatch_resume

dispatch_queue_create で作成した Dispatch Queue のサスペンドとその復帰ができる。 Global Dispatch Queue などには効かない。

dispatch_queue_t q = dispatch_queue_create(0, DISPATCH_QUEUE_CONCURRENT);

// dispatch_suspend(q);

__block NSInteger num = 0;
for (int i = 0; i < 100; i++) {
  dispatch_async(q, ^{ NSLog(@"%d", num); });
}
[NSThread sleepForTimeInterval:0.01];
num = 100;

// dispatch_resume(q);

dispatch_release(q);

上のコードを実行すると出力される数値は 0 か 100 だけど、 コメントアウトを外すとすべて 100 になる。

dispatch_once

一度しか実行されない特殊な dispatch_sync? シングルトンの生成など、一度だけ同期的に処理を行いたい場合につかう。 dispatch_once に渡したブロックは一度だけ処理される。 このブロックの処理が終了するまでは、他のスレッドは dispatch_once で停止する。 以下、ARC有効時における簡易なシングルトン生成方法。

きちんと実装する場合はGCDのdispatch_onceを使ったシングルトン(ARC対応版) - メモてきなあれと,日記てきなあれ. らしい。

//
// (under ARC)
//

static id sharedInstance_ = nil;

@implementation MySingleton

+ (id)sharedInstance {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sharedInstance_ = [[self alloc] init];
  });

  NSAssert(sharedInstance_, @"sharedInstance_ must not be nil.");
  return sharedInstance_;
}

+ (id)allocWithZone:(NSZone *)zone {
  __block id ret = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    ret = [super allocWithZone:zone];
  });

  NSAssert(ret, @"ret must not be nil.");
  return ret;
}

alloc & init を最初に実行した後に sharedInstance を実行すると NSAssert で止まるようにしてる。 sharedInstance を最初に実行した場合は 移行の allocnil を返す。 NSAssert でデバッグ時に止めておけば リリース時に alloc & init するようなコードが残ることはなかなかないだろうしこれでいいよね… alloc & init されたオブジェクトを保持されて使い回され、かつ sharedInstance が一切コールされない場合はダメだけどそれはちょっと考えにくい訳で。。

Dispatch Semaphore

dispatch_semaphore_create(long initial_count) でセマフォを生成し、 dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) で信号待ちし、 dispatch_semaphore_signal(dispatch_semaphore_t dsema) で旗を1つあげる。

//
// 非ARC
//

// dispatch_semaphore_create(初期のカウンタ値) でセマフォを生成
// Dispatch Semaphore は計数型セマフォ
const long FirstCount = 1;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(FirstCount);

dispatch_queue_t q = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);

NSMutableString *string = [[NSMutableString alloc] initWithCapacity:100];

dispatch_apply(100, q, ^(size_t index) {
  // 信号待ち
  // カウンタが 0 の場合は通過待ちで待機(この場合はタイムアウト無し)
  // カウンタが 1以上 の場合 or 待機中に1以上となった場合 に通過(通過時にカウンタ1減る)
  dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

  // ここが複数スレッドから実行されることは無い
  // セマフォで制御しない場合は appendFormat で高確率でエラー発生
  [string appendFormat:@"%zd", index % 10];

  // カウンタを1増やす
  dispatch_semaphore_signal(semaphore);
});

NSLog(@"%@", string);
assert(string.length == 100);

dispatch_release(q);
dispatch_release(semaphore); // 使い終わったらセマフォも解放
//
// Under ARC
//
dispatch_queue_t queue
  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

dispatch_async(queue, ^ {
  NSLog(@"start sleep 3 seconds");
  sleep(3);
  NSLog(@"end sleep");
  dispatch_semaphore_signal(semaphore);
});

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

NSLog(@"Done"); // 3秒後に実行される

Dispatch Semaphore でハマる(というか原因は dispatch_apply だった)

dispatch_semaphore_create の引数は初期カウンタ値らしいんだけど、 2以上を指定したときになんだか思うように動かなかった。 具体的には以下のような感じになる。

//
// Under ARC
//
dispatch_queue_t queue
= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);

dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC);

printf("Main    Thread = %p \n", [NSThread mainThread]);
dispatch_apply(100, queue, ^(size_t index) {
  if (dispatch_semaphore_wait(semaphore, timeout) == 0) {
    printf("Current Thread = %p \n", [NSThread currentThread]);

    sleep(1);
    printf("IsMainThread = %s \n",
          [NSThread mainThread] == [NSThread currentThread] ? "YES" : "NO");
  } else {
    // 通過待ちタイムアウト…通ってくれない。
    NSLog(@"Timeout.");
  }
});

結果

Main    Thread = 0x1d5306e0
Current Thread = 0x1d5306e0
Current Thread = 0x1d563530
IsMainThread = NO
Current Thread = 0x1d563530
IsMainThread = YES
Current Thread = 0x1d5306e0
IsMainThread = NO
Current Thread = 0x1d563530
IsMainThread = YES
Current Thread = 0x1d5306e0
IsMainThread = YES
Current Thread = 0x1d5306e0
IsMainThread = NO
Current Thread = 0x1d563530
(以下省略)

タイムアウトしてくれないし、そもそも2スレッドしか動いてない。 原因は dispatch_apply を使用していることだった。 dispatch_semaphore_wait で待ち状態のスレッドは停止するが、dispatch_apply は2スレッドしか並列実行しない。 したがって、dispatch_applyDispatch Semaphore を組み合わせるときは カウンタ数が2以上だと待ち状態が発生しないので当然タイムアウトも発生しないという落ちだった。

ということで以下のようにすると期待通りの動作をしてくれる。

//
// 非ARC
//
dispatch_queue_t queue
  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);

dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC);

for(int i = 0; i < 10; i++) {
  dispatch_async(queue, ^{
    if (dispatch_semaphore_wait(semaphore, timeout) == 0) {
      NSLog(@"Passed %p", [NSThread currentThread]);
      sleep(1);
      dispatch_semaphore_signal(semaphore);
    } else {
      // 通過待ちタイムアウト
      NSLog(@"Timeout %p", [NSThread currentThread]);
    }
  });
}

dispatch_barrier_async(queue, ^{
  NSLog(@"Done %p", [NSThread currentThread]);
});

結果

Passed 0x1f588290
Timeout 0x2004e5d0
Passed 0x1f588810
Passed 0x2004e2e0
Timeout 0x2004e5d0
Timeout 0x2004e5d0
Timeout 0x1f588550
Timeout 0x1f588550
Timeout 0x2004e5d0
Timeout 0x1f588550
Done 0x2004e5d0
Share
関連記事