Objective CでのGCD

Overview


Objective Cでのマルチコアプロセッシング技術であるGCDと、GCDの基盤であるBlock Coding、そしてこれらを有したLLVMについて調べてみよう。

まさしくLLVMの時代

現在(2016/6/15時点)は、LLVMの時代といっても過言ではありません。
Linux、Windows、Mac OSX、そうそうたるOSがすべてLLVMを選択し、使用しているからです。LLVMとは、Low Level Virtual Machineの略で、簡単に言うとコンパイラです。コンピュータを初めて学ぶ方が多く扱うコンパイラは、主にGCCでしょう。このGCCの代替コンパイラ系の筆頭がまさにLLVMです。

<図1 LLVM構造>

LLVMはGCCが持つ限界を克服したC++基盤の新しいコンパイラを作成することを目的に始まりました。これらのプロジェクトをAppleは積極的に支援しています。AppleではXcode3以前はGCCを利用し、以降はLLVMに移行して、多くのパフォーマンスと機能の向上に効果を上げました。その内容は次のとおりです。

  • コンパイル速度の向上
  • 実行ファイルのサイズ縮小
  • 実行速度の増加
  • 今回の主要テーマであるGCD、Block
  • 便利になったXcodeの編集機能、リアルタイムエラー

<図2 GCDにより便利になったXcode>

次章では、LLVM基盤で生まれ変わったGCDについて調べてみよう。

GCD(Grand Central Dispatcher)

GCDは最初に紹介したように、マルチコアプロセッサ向けのThreadプログラミング方法(C Library)です。既存のスレッド管理手法は、開発者が直接ロックを掛けて、スレッドプールを管理するなどの手間が必要でしたが、GCDはThreadをOSが自動で管理や分配をしてくれます。

以前は、スレッドを直接作成して管理するなどの仕事を開発者が直接操作していましたが、GCDでは、Dispatch QueueにTaskを作成して投げるだけです。

Dispatch Queue

Dispatch QueueはTaskを積載するデータ構造です。データ構造のQueueの動作がSerial(順次)でもConcurrent(同時)でも、常にFIFO(First In First Out)方式で動作します。

  • Serial Dispatch Queue:QueueにPushされた順に1つずつTaskを実行し、そのTaskが終了するまで待機する。Queueを複数作成することもでき、各QueueはConcurrencyに戻る。
  • Concurrent Dispatch Queue:Global Dispatch Queueとも呼ばれ、複数のtaskをConcurrentで実行する。実行順序は、Pushされた順序で実行され、同時に実行されるTaskは、システム環境に依存する。
  • Main Dispatch Queue:ApplicationのMain ThreadのSerialで実行されるTaskを管理するQueue。当該Queueは、ApplicationのRun loopで動作することになる。

では、これらのGCDを使用する際に必要なBlock Codingについて見てみよう。

Block Coding

Blockは、他の言語のLambdaやClosureの概念と似た部分が多いです。そのため、スコープに対して変数/環境をCapturingすることもできます。

Block Syntax

ブロックは、次のように作成できます。

^{
    NSLog(@"This is a Block");
}

上記のようなブロックを変数に代入することもできます。

void (^completion)(void) = ^{
    NSLog(@"This is a Block");
}

// invoke
completion();

パラメータを受け取ったり、値を返すこともできます。

NSString* (^sayHelloBlock)(NSString*, NSString*) = ^(NSString *name, NSString *country) {
    NSString *helloStr = nil;
    if (country caseInsensitiveCompare:@"kr"] == NSOrderedSame) {
        helloStr = [NSString stringWithFormat:@"%@, お会いできてうれしいです。", name];
    } else if (country caseInsensitiveCompare:@"en"] == NSOrderedSame) {
        helloStr = [NSString stringWithFormat:@"%@, Nice to meet you.", name];
    } else {
        helloStr = [NSString stringWithFormat:@"%@, Where are you from?", name];
    }

    return helloStr;
}


NSLog(@"%@", sayHelloBlock(@"Panki", @"KR"));

次のように同じLexical Scope(Enclosing Scope)の変数をCapturingすることもできます。

int testA = 89;

void (^testBlock)(void) = ^{
    NSLog(@"testA is: %d", testA);
};

testA = 4;

testBlock();

///// OUTPUT
> testA is: 89

__blockキーワードを作成して、Capturingした変数を変更したり、変更を共有することもできます。

__block int testA = 89;
__block int testB = 91;

void (^testBlock)(void) = ^{
    NSLog(@"testA is: %d", testA);
    NSLog(@"testB is: %d", testB);    
    testB = 77;

};

testA = 4;
testBlock();
NSLog(@"Value of original A is now: %i", testA);
NSLog(@"Value of original A is now: %i", testB);

///// OUTPUT
> testA is: 89
> testA is: 91
> Value of original A is now: 4
> Value of original B is now: 77

これらのBlockをメソッドのパラメータとして渡すことができます。

- (void)doSomethingWithCompletion:(void (^)(int, int)) completion {
    // Do Sth.
    ...

    completion(28, 89);
}



// 外部からのcall
[self doSomethingWithCompletion:(void(^)(int a, int b)) {
    int c = a+b;
    // Do Anything
}];

CallBackとよく似ていますね。
Blockの説明はここまでにして、本格的にBlockを使ってGCDを使ってみよう。

GCD Usage

1. Dispatch Queueの取得、または作成

// 1) Dispatch Queueを生成する。
// 1-1) serial dispatch queueを生成する。
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
// 1-2) concurrent dispatch queueを生成する。
dispatch_queue_t concurrentQueue = dispatch_queue_create(""test", DISPATCH_QUEUE_CONCURRENT);

// 2) Main Dispatch Queueを取得する。
dispatch_queue_t mainQueue = dispatch_get_main_queue();

// 3) Global Dispatch Queueを取得する。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

2. Dispatch QueueにTask追加

Taskは2つの方法で追加できます。

  1. SyncにTask追加
  2. AsyncにTask追加

Dispatch Queueは、Serial / Concurrentの2つ(Mainはserial、Globalはconcurrentに動作)がありますので、Taskの追加方法によって、次のように4つのケースになります。

  1. Serial Dispatch QueueにSyncするようにTask追加
  2. Serial Dispatch QueueにAsyncするようにTask追加
  3. Async Dispatch QueueにSyncするようにTask追加
  4. Async Dispatch QueueにAsyncするようにTask追加

例を見ながら、それぞれの状況について説明しよう。
-1. Serial Dispatch QueueにSyncするようにTask追加

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{ NSLog(@"1"); });
dispatch_sync(queue, ^{ NSLog(@"2"); });
dispatch_sync(queue, ^{ NSLog(@"3"); });

Serial Queueを作成した後、上記のようにSyncしてTaskを追加すると、入れた順番に実行されるので、次のような結果になります。

1
2
3

-2. Serial Dispatch QueueにAsyncするようにTask追加

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ NSLog(@"1"); });
dispatch_async(queue, ^{ NSLog(@"2"); });
dispatch_async(queue, ^{ NSLog(@"3"); });

Serial Queueを作成した後、上記のようにAsyncしてTaskを追加しても、結局はSerialに1つずつTaskを実行し、他のTaskの追加を先に入ったTaskの実行までBlockingするので、上記のように同じ実行順序を示します。

1
2
3

-3. Async Dispatch QueueにSyncするようにTask追加

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{ NSLog(@"1"); });
dispatch_sync(queue, ^{ NSLog(@"2"); });
dispatch_sync(queue, ^{ NSLog(@"3"); });

上記のようにConcurrent Queueを作成した後、SyncにTaskを追加すると、各Taskが実行されるまでTaskを追加しないので、以下のような結果になります。

1
2
3

-4. Async Dispatch QueueにAsyncするようにTask追加

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ NSLog(@"1"); });
dispatch_async(queue, ^{ NSLog(@"2"); });
dispatch_async(queue, ^{ NSLog(@"3"); });

Concurrent QueueAsyncするようにTaskを追加すると、各TaskがQueueに入って、特定の個数が同時実行されます。そのため次のような結果(状況に応じて変わる)を取得します。

1
3
2

3. Dispatch QueueにTimerを追加

Dispatch Queueに特定の時間以降にTaskを追加する方法について説明します。

特定の時間以降に、Dispatch QueueにTaskを追加するには、disaptch_afterを使用します。

dispatch_queue_t queue = dispatch_queue_create("test",  DISPATCH_QUEUE_SERIAL);

double delayAfter = 3.0;
dispatch_time_t pushTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayAfter * NSEC_PER_SEC));

dispatch_after(pushTime, queue, ^{ NSLog(@"1"); });
dispatch_async(queue, ^{ NSLog(@"2"); });
dispatch_async(queue, ^{ NSLog(@"3"); });

上記のような構文を実行すると、3秒後にQueueが追加されて実行されますので、次のような結果になります。

2
3
1

  • Dispatch QueueにTaskを追加するという意味は、特定の時間以降に実行されるという意味ではありません。Task追加時間と実行時間は明確に区別して理解する必要があります。

4. Dispatch Queueの優先順位

Global QueueやMain Queueを除く、ユーザーの作成キュー(dispatch_queue_create(…)の優先順位は、Global QueueのDefault Queueとその優先順位が同じです。これらの優先順位を変更するときは、dispatch_set_target_queueを使用します。

dispatch_queue_t queueHigh = dispatch_queue_create("test1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueLow = dispatch_queue_create("test2", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t globalQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_set_target_queue(queueHigh, globalQueueHigh);

dispatch_queue_t globalQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_set_target_queue(queueLow, globalQueueLow);

dispatch_async(queueLow, ^{ NSLog(@"1"); });
dispatch_async(queueHigh, ^{ NSLog(@"2"); });
dispatch_async(queueLow, ^{ NSLog(@"3"); });
dispatch_async(queueHigh, ^{ NSLog(@"4"); });

上記のような構文を実行すると、queueHighの優先順位が高く割り当てられるので、次のような結果になります。

2
4
1
3

5. Dispatch Group

一連のTaskを同じグループにまとめて処理したいときはどうすればよいでしょうか?また、その一連のTaskが作業をすべて実行した後に、どのようにNoticeを受け取ることができるでしょうか?以下の機能がこのような悩みを解決してくれます。
dispatch_group_create, dispatch_group_async, dispatch_group_notify

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, queue, ^{ NSLog(@"1"); });
dispatch_group_async(group, queue, ^{ NSLog(@"2"); });
dispatch_async(queue, ^{ NSLog(@"3"); });
dispatch_group_notify(group, queue, ^{ NSLog(@"作業がすべて完了しました。"); });
dispatch_async(queue, ^{ NSLog(@"4"); });
dispatch_async(queue, ^{ NSLog(@"5"); });

上記の操作を実行すると、結果は次のようになります。(状況によって変わるかもしれませんが、1つのグループ内で作業が終了した後、notifyされる順序は保証されます。)

2
1
3
5
操作がすべて完了
4

6. Dispatch Queueを利用したシングルトン生成

開発時にはシングルトンに接する機会が多いですが、GCDはシングルトンを生成する方法も提供しています。disipatch_onceがまさしくそれです。

- (MyCustomClass *)sharedInstance {
    static dispatch_once_t onceToken;
    static MyCustomClass *instance = nil;

    dispatch_once(&onceToken, ^{
        instance = [[MyCustomClass alloc] init];
    });

    return instance;
}

おわりに

GCDの使用方法をいくつか説明しました。Dispatch Queueを生成し、QueueにTaskをAsyncあるいはSyncにPushする方法、一定時間後にQueueにTaskを追加する方法、シングルトンを作成する方法などを紹介しました。

この記事で説明していないGCDの内容もあります。例えば、dispatch_applydispatch_barrier_async、 dispatch_barrier_syncセマフォを扱うdispatch_semaphore_tなどについては、機会があれば次回の記事で取り上げたいと思います。

また、Dispatch Queueを使用するだけが正解ではありません。直接的にThreadを管理する場合もあり、NSOperationQueueを使用する方法もあります。しかし、コードの簡潔性と直感性、そして何よりも、AppleからThreadを直接ハンドリングしないようにするなどの理由からGCDを推奨したいと思います。

Reference

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop