-
Notifications
You must be signed in to change notification settings - Fork 336
8.2 Grand Central Dispatch
この章では、iOSにおけるマルチスレッドの手法であるGCD(Grand Central Dispatch)の使い方と、使う上での注意点について解説します
この章では、次のAppleのドキュメントを参考に作成しています。 https://developer.apple.com/jp/devcenter/ios/library/documentation/ConcurrencyProgrammingGuide.pdf
iOSにおける並列処理の手法にはいくつかの手法があります。
- NSThread : スレッドを立てて、そのスレッドの中で処理を行う。スレッドの管理やキューイングなどの管理はアプリケーションが行う
- GCD (dispatch_xxx) : スレッドの管理などをOSレベルで実装したもの。処理をしたいタスクをBlockで渡す。渡されたタスクはキューに挿入されて逐次実行される。C言語による実装
- NSOperation : GCDと同じように振る舞う、Objective-Cのオブジェクト。
この中で、GCDがよく利用されています。NSThreadだとスレッドの処理などを自前で書かないといけないのでコードが冗長になってしまい大変です。
そこで今回は、よく用いられるiOSにおける並列処理実行技術、GCDについて解説を進めて行きます。
3G回線などのよくない通信環境において使われるスマートフォンは、当然ながら通信の処理に時間を要します。またファイルへのアクセスやDBへのアクセスなどでも待ち時間が発生します。これらの処理を画面を描画するスレッド(iOSではメインスレッド)で行った場合、画面描画やユーザーのアクションへの反応ができなくなり、いわゆる"固まった"状態になってしまいます。(AppStoreの審査基準にも、起動後10秒以内にユーザーに何かしらの情報を提示しないとリジェクトの対象となる、とあります。)
一方で、最近のスマートフォンにはマルチコアのCPUが搭載されており、iPhone5に搭載されているApple A6プロセッサもコアを二つ搭載しています。しかし、並列して処理を行わない限り、各ステップは一つのスレッド上で実行されるので、複数のコアを持つ場合は持て余してしまいます。
そこで、待ち時間の発生する処理は別のスレッドで処理を行うことで、アプリケーションの応答性を上げることができ、また計算に時間のかかる処理などは複数のスレッドを用いて並列に処理を行うことでCPUのパワーを存分に発揮することができます。
こういった観点から、スマートフォンにおける並列処理は不可欠なのです。
スマートフォンアプリにおいて、並列処理は必要不可欠ですが、スレッドのコードをアプリ内で記述するのはとても骨が折れます。そこで、並列処理を簡単に行うことのできる仕組みとしてGCDが登場しました。iOS4以降で利用することができます。
GCDの仕組みを簡単に説明すると
- dispatch queue というキューに処理を行いたい内容(タスク)を追加して行く
- キューに追加されたタスクはFIFOで順番に実行される
- タスクの実行は別スレッド上で実行される
となります。またさらに
- タスクはBlocksとして登録する
- キューの管理はアプリケーション独自に作ったもの、アプリケーション起動時にシステム内で自動的に生成されるもの、の二種類がある
- 適切なスレッドへのタスクの振り分けはOSが行う
- アプリケーション内部で自動生成されたキューと、そのキューに追加されたタスクを実行するスレッドの管理はOSが自動的に行ってくれる
という特徴があります。
図にするとこんなイメージです。
ディスパッチキューを利用するにあたって必要なことはリファレンスに次のように述べられています
タスクを非同期に実行する技術のひとつとして、Grand Central Dispatch(GCD)というものがありま す。通常はアプリケーション中に記述するスレッド管理用のコードを、システムレベルで実装したも のです。開発者がしなければならないのは、実行したいタスクを定義し、適切なディスパッチキュー に追加することだけです。するとGCDは、必要なスレッドを生成し、そこでタスクを実行するよう適 切にスケジューリングします。
スレッドの生成や管理はシステムがやってくれるので、開発を進めて行く上で必要なことは、
- 実行したいタスクを定義する
- 適切なディスパッチキューに追加する
の2点になります。次項以降で、キューへの追加の方法と適切なキューの選び方を説明します。 またあわせて、GCDで利用できる他のAPIについても解説します.
dispatch queue への追加には次の関数を使います。
関数名 | 説明 |
---|---|
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block) |
blockで定義された処理をqueueに追加する。タスクの処理は非同期で実行され、タスクの実行完了を待たずに制御は進む |
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block) |
blockで定義された処理をqueueに追加する。タスクの処理は同期的に実行され、タスクの実行が完了するまで制御は止まる |
それぞれの関数の引数は、処理をキューイングしたいキューを指定するqueueと、処理したい内容を記述したブロックblockです。 実際に用いてみると次のようになります
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSLog(@"Hello GCD!");
});
この例では、queueとして優先度がバックグラウンドのglobal queueを取得しています(後述します)。また処理内容は、"Hello GCD!"と出力するものを指定しました。
これを適当な箇所で実行すると、コンソールに"Hello GCD!"と表示されます
dispatch queueで用いられるキューにはいくつかの種類があります。まず大きく分類してみましょう。
キューの種類 | 説明 |
---|---|
直列 serial dispatch queue | タスクを同時に一つずつ追加された順に実行する仕組み。あるタスクの実行が終わると次のタスクの実行を行う。タスクは他のキューと独立したスレッド上で動作。 |
並列 concurrent dispatch queue | 複数のタスクを同時に実行する。実行の順番はキューに追加した順番になるが、終了のタイミングの順序は保証されない。同時に実行するタスクの数はシステムの状況に応じて変化する。アプリケーションが所有するglobal queueとアプリケーション内部で生成するqueueを持つことができる。キューには優先度をつけることができる |
メインディスパッチキュー | アプリケーションのどこからでも利用することの出来るシリアルキューで、アプリケーションのメインスレッド上で実行される。UIの更新などはこのキューを用いて行う必要があります |
serial dispatch queue と concurrent dispatch queue の違いは以下のようになります
serial queue は同時に一つしか実行されないので、一つ前のタスクの結果によって次のタスクの処理が変わる場合に用いられることが多いです。例えばファイルやDBへのアクセスなどがそれに該当します。mixiの公式クライアントアプリではアクセストークンの更新などにも用いられています。
一方でconcurrent queue は同時にいくつものタスクを並列に実行することができます。処理一つ一つが前回のタスクに依存しないとき、他のスレッドとデータを共有しないときなどで幅広く用いることができます。例えばダウンローダなどはこの仕組みを利用することができると思います。
これらのキューはどのようにして取得することができるのでしょうか、その点について次に説明します。
各アプリケーションには初めから優先度の異なる4つのグローバルキューが生成されており、アプリケーションのどこからでも取得できるようになっています。 次の関数で取得することができます。
dispatch_queue_t dispatch_get_global_queue(long priority, unsigned long flags);
priorityには次のうち一つを指定してください。
キー名 | 優先度 |
---|---|
DISPATCH_QUEUE_PRIORITY_HIGH | 優先度高 |
DISPATCH_QUEUE_PRIORITY_DEFAULT | 優先度中 |
DISPATCH_QUEUE_PRIORITY_LOW | 優先度低 |
DISPATCH_QUEUE_PRIORITY_BACKGROUND | 優先度バックグラウンド |
flagsには今のところ0を指定しておいてください(今のところは利用されていませんが、後々利用される可能性があるため)
メインキューを取得するには次の関数を利用します。
dispatch_queue_t dispatch_get_main_queue(void);
キューはグローバルなキューを取得するだけでなく、アプリケーションの一部のみで利用されるキューをアプリケーション内部で生成して利用することも出来ます。直列、並列なキューのどちらも生成することができます。
生成には次の関数を使います。
dispatch_queue_t dispatch_queue_create( const char *label, dispatch_queue_attr_t attr);
labelには、キューのラベルをつけます。システム中で用いることはありませんが、デバッグを行った時のスタックトレースなどに出力されます。Appleのリファレンスによると、逆DNS記法でつけることが推奨されています(例:com.example.myqueue)
dispatch_queue_attr_tでは、キューのタイプが直列か並列を指定します。(iOS5以降) 直列の場合はDISPATCH_QUEUE_SERIALを、並列の場合はDISPATCH_QUEUE_CONCURRENTを指定します。
実際にGCDを用いて、どのように動作するかを確かめます。 適当なサンプルプロジェクトを作り、最初に表示されるviewControllerのviewDidLoadに書いていきます
検証は、
- 並列なキュー と 直列なキュー
- dispatch_async と dispatch_sync
をそれぞれ使った時にどうなるかを検証します。タスクの内容はコンソールにログを吐き出すだけとします。
並列に実行するので、実行の順番は保証されません。またasyncなので、タスクをキューに追加したら完了まで待ちません。
実行したコード
for (NSInteger i = 0 ; i < 100; ++i) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSLog(@"%d", i);
});
}
NSLog(@"here!!");
ログのうち、最初の11行を抜粋
2013-05-08 12:49:08.569 GCDSample[12312:3c07] 2
2013-05-08 12:49:08.569 GCDSample[12312:4203] 6
2013-05-08 12:49:08.569 GCDSample[12312:4303] 7
2013-05-08 12:49:08.569 GCDSample[12312:1d03] 0
2013-05-08 12:49:08.569 GCDSample[12312:c07] here!!
2013-05-08 12:49:08.569 GCDSample[12312:4403] 8
2013-05-08 12:49:08.569 GCDSample[12312:3f03] 3
2013-05-08 12:49:08.569 GCDSample[12312:4103] 5
2013-05-08 12:49:08.569 GCDSample[12312:1303] 1
2013-05-08 12:49:08.569 GCDSample[12312:4003] 4
2013-05-08 12:49:08.574 GCDSample[12312:3c07] 9
順番に実行されていること、"here!!"が全てのタスクの完了より前に来ていることがわかるかと思います。
あるタスクは時間もかかり、かつその後の処理でこのタスクの結果を用いない、といったケースに向いています。
並列なキューなので、実行が完了する順番は保証されません。dispatch_syncを用いているので、完了するまでキューに追加したスレッドは停止します。タスクの実行は本来まちまちになりますが、タスクの完了まで処理が進まないので、結果的に全てのタスクが順番に実行されます。
for (NSInteger i = 0 ; i < 100; ++i) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
NSLog(@"%d", i);
});
}
NSLog(@"here!!");
コンソールログ
2013-05-08 13:01:13.691 GCDSample[12490:c07] 0
2013-05-08 13:01:13.692 GCDSample[12490:c07] 1
2013-05-08 13:01:13.692 GCDSample[12490:c07] 2
2013-05-08 13:01:13.692 GCDSample[12490:c07] 3
~~ 中略 ~~
2013-05-08 13:01:13.746 GCDSample[12490:c07] 98
2013-05-08 13:01:13.746 GCDSample[12490:c07] 99
2013-05-08 13:01:13.746 GCDSample[12490:c07] here!!
追加したタスクが実行完了するまで次に進まないので、"here!!"は最後に出力されています。
dispatch_queueで行った内容を後ほど使う必要がある、と言う場合に使います。 一般的には、dispatch_syncを使わなくてもタスクを実行しないと次のステップに進まないのであえて使う必要はあまりないと思います。 例えば、タスクの内容を別スレッドにあえて投げて、現在実行中のスレッドを別のタスクで利用できるようにする、といった使い方はできるかもしれません。
直列なキューでは同時に実行されるタスクの数が1つに限られているので順番が前後することはありません。またasyncとしているので、キューにタスクを追加したらコードは次に進みます。
dispatch_queue_t queue = dispatch_queue_create("jp.mixi.ios.sample", DISPATCH_QUEUE_SERIAL);
for (NSInteger i = 0 ; i < 100; ++i) {
dispatch_async(queue, ^{
NSLog(@"%d", i);
});
}
NSLog(@"here!!");
2013-05-08 13:08:00.910 GCDSample[12596:1303] 0
2013-05-08 13:08:00.910 GCDSample[12596:c07] here!!
2013-05-08 13:08:00.912 GCDSample[12596:1303] 1
2013-05-08 13:08:00.912 GCDSample[12596:1303] 2
2013-05-08 13:08:00.913 GCDSample[12596:1303] 3
2013-05-08 13:08:00.913 GCDSample[12596:1303] 4
2013-05-08 13:08:00.913 GCDSample[12596:1303] 5
2013-05-08 13:08:00.914 GCDSample[12596:1303] 6
出力は順番になっており、タスクを追加し終わると次に進んでいるのが分かるかと思います。
直列なキューを使うので、キューでの処理内容に一意性が求められる時に使います。例えばファイルへのアクセスなどが該当します。ファイルへのアクセスなどは時間がかかるのでasyncで非同期実行するといった使い方ができます
直列なキューではタスクが同時に一つしか実行しないので競合が起きるなどの心配がなく、またdispatch_syncを用いるのでタスクの終了まで次のステップに進みません。
dispatch_queue_t queue = dispatch_queue_create("jp.mixi.ios.sample", DISPATCH_QUEUE_SERIAL);
for (NSInteger i = 0 ; i < 100; ++i) {
dispatch_sync(queue, ^{
NSLog(@"%d", i);
});
}
NSLog(@"here!!");
2013-05-08 13:01:13.691 GCDSample[12490:c07] 0
2013-05-08 13:01:13.692 GCDSample[12490:c07] 1
2013-05-08 13:01:13.692 GCDSample[12490:c07] 2
2013-05-08 13:01:13.692 GCDSample[12490:c07] 3
~~ 中略 ~~
2013-05-08 13:01:13.746 GCDSample[12490:c07] 98
2013-05-08 13:01:13.746 GCDSample[12490:c07] 99
2013-05-08 13:01:13.746 GCDSample[12490:c07] here!!
ファイルはDBなどのように不整合が起きうる箇所で、かつその読み込んだ内容を次で使う、といった場合に使います
いくつかの処理を分散して行いたい、そして分散して処理を行った結果をまとめて利用したい、といった時に利用できるのがdispatch groupです。 dispatch group の利用の手順は以下のようになります。
- dispatch_groupの作成
- groupとqueueを指定して、タスクをqueueに追加
- dispatch_group_wait関数を用いてタスクの終了まで待機
- dispatch_groupの解放
以下にサンプルを示します。
dispatch_group_t group = dispatch_group_create(); // 1. dispatch group の生成
for (NSInteger i = 0 ; i < 100; ++i) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2. タスクをdispatch_queue に追加
dispatch_group_async(group, queue, ^{
NSLog(@"%d", i);
});
}
// 3. タスクの終了まで待機
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"here!!");
// 4. 不要になったgroupをrelease
dispatch_release(group);
この時のコンソールへのログは以下のようになります。
2013-05-08 15:51:15.778 GCDSample[14060:1303] 1
2013-05-08 15:51:15.778 GCDSample[14060:4203] 5
2013-05-08 15:51:15.778 GCDSample[14060:1b03] 0
2013-05-08 15:51:15.778 GCDSample[14060:4403] 7
~~~ 中略 ~~~
2013-05-08 15:51:15.825 GCDSample[14060:5403] 98
2013-05-08 15:51:15.825 GCDSample[14060:3c07] 99
2013-05-08 15:51:15.830 GCDSample[14060:c07] here!!
例えば、ファイルへの分散の読み込みなどが使用例としてあげられます。
SERIAL_DISPATCH_QUEUEを用いることで一意性を保証することができます。しかし、例えばファイルへのアクセスなどでは、読み込みに関しては並列に行い、書き込みに関しては直列で行いたい、といった状況が想定できます。そのような時に用いるのがdispatch_barrier_asyncです。barrier_asyncを使って追加されたタスクについては同時に実行されるタスクの数は1つのみとなります。
例えば、以下のような0..99までのループで10の剰余が0のときに文字列を書き換え、それ以外の時はその文字列をprintするサンプルを考えます。
for (NSInteger i = 0; i < 100; ++i) {
if (i%10 == 0) {
[string deleteCharactersInRange:NSMakeRange(0, string.length)];
[string appendFormat:@"%02d", i];
} else {
NSLog(@"%02d : string = %@", i, string);
}
}
この処理をdispatch_queueを使って並列に処理してみます。barrier asyncを使わずに書くと次のようになります。
dispatch_queue_t queue = dispatch_queue_create("jp.mixi.ios.sample.barrier", DISPATCH_QUEUE_CONCURRENT);
NSMutableString *string = [NSMutableString string];
for (NSInteger i = 0 ; i < 100; ++i) {
if (i % 10 == 0) { // 書き込み処理
dispatch_async(queue, ^{
[string deleteCharactersInRange:NSMakeRange(0, string.length)];
[string appendFormat:@"%02d", i];
});
} else { // 読み込み処理
dispatch_async(queue, ^{
NSLog(@"%02d : string = %@", i, string);
});
}
}
この時の処理結果は次のようになります
2013-05-08 16:28:52.964 GCDSample[14760:1303] 01 : string = 00
2013-05-08 16:28:52.969 GCDSample[14760:3f03] 09 : string =
2013-05-08 16:28:52.969 GCDSample[14760:4003] 11 : string = 10
2013-05-08 16:28:52.969 GCDSample[14760:4303] 12 : string = 10
09のタイミングではおそらく、deleteCharactersInRangeが完了したタイミングで呼ばれているのだと思います。このようにデータの不整合などを引き起こしかねません。
barrier_asyncを使うと次のようになります。
dispatch_queue_t queue = dispatch_queue_create("jp.mixi.ios.sample.barrier", DISPATCH_QUEUE_CONCURRENT);
NSMutableString *string = [NSMutableString string];
for (NSInteger i = 0 ; i < 100; ++i) {
if (i % 10 == 0) { // 書き込み処理
// ここを変えた
dispatch_barrier_async(queue, ^{
[string deleteCharactersInRange:NSMakeRange(0, string.length)];
[string appendFormat:@"%02d", i];
});
} else { // 読み込み処理
dispatch_async(queue, ^{
NSLog(@"%02d : string = %@", i, string);
});
}
}
この時の結果は次のようになります。
2013-05-08 16:31:34.891 GCDSample[14813:4103] 06 : string = 00
2013-05-08 16:31:34.896 GCDSample[14813:4003] 09 : string = 00
2013-05-08 16:31:34.897 GCDSample[14813:1303] 12 : string = 10
2013-05-08 16:31:34.897 GCDSample[14813:4003] 11 : string = 10
2013-05-08 16:31:34.897 GCDSample[14813:1e03] 13 : string = 10
2013-05-08 16:31:34.897 GCDSample[14813:3d07] 14 : string = 10
データの不整合が起きていないことが分かると思います。このようにdispatch_barrier_asyncはデータの書き込みなど、不整合を防ぐ目的で使うことができます。
dispatch once はプログラム実行中に一度だけ実行されることを期待する箇所で使います。よく用いられるのがシングルトンクラスの初期化です。 シングルトンクラスの初期化においては、次のように用いることが多いです。
static SingletonClass *sharedInstance = nil;
+ (SingletonClass*)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
// その他の初期化をここで行う
});
return sharedInstance;
}
このように記述した場合、ブロック内のコードは一度しか実行されません。また他のスレッドからこのメソッドにアクセスした場合、dispatch_onceの箇所でステップは停止します。
マルチスレッドプログラミングを行う際には、シングルスレッドの場合とは異なる注意点がいくつか存在します。 ここではその注意点のうち、注意したいものを紹介します。
複数のタスクが、同一のデータソース(ファイルやデータベースなど)にアクセスする際に、データの整合性が取れなくなることです。あるスレッドから読み込んだデータが実は他のスレッドでは書き込み中だった、といった時などに発生します。 上記、dispatch_barrier_asyncなどの例がデータに競合が起こっている例です。
回避策としては、
- dispatch_barrier_async
- 直列なキュー
などを使うことで同時にアクセスできるスレッドを制御するという方法があげられます。さらに粒度の高い制御が必要な場合、ディスパッチセマフォという機構も用意されています。
デッドロックとは、複数のスレッドがお互いのスレッドの終了を待っているため、結果的にどちらの処理も進めることができなくなってしまうことを指します。GCDを用いる場合デッドロックを簡単に引き起こしてしまうので注意が必要です。特にdispatch_syncなどの処理を待つ使い方をする場合は注意が必要です。
たとえば次のようなコードはデッドロックを引き起こします。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"a");
dispatch_sync(queue, ^{
NSLog(@"b");
});
NSLog(@"c");
});
NSLog(@"d");
'a'を出力したところで止まってしまいます。シリアルなキューqueueに対して、タスクを挿入して、さらに同じqueueにsyncでタスクを挿入しています。 タスクを追加して終了を待つも、自分自身も同じqueueの中で動いているので、そのタスク自体開始することができないので、処理が止まってしまいます。
回避するには、あるスレッドから同じスレッドのdispatch_syncを呼び出さないようにすることが必要です。そのために、必要以上にdispatch_syncを使わず、出来る限りdispatch_asyncを使うことが推奨されます。
並列で複数の処理を行いたい場合、次のようにGCDを使うことは推奨されていません。
for (NSInteger i = 0 ; i < 100; ++i) {
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"%d", i);
});
}
この例では、直列なキューを100個作り、そのキューの中にタスクを追加しています。キューを生成するということはコストの高い演算となります。シリアルなキューはリソースの保護やアプリケーション内での同期を計りたいなどの際に利用してください。純粋に並列実行する場合ならconcurrent queue を用いた方が効率が良いです。
また、生成したキューに関するメモリ管理はリファレンスカウンタ方式を用いてカウントしています。iOS6以降ではARCによる自動管理がされますが、6未満の場合は自動的に管理されません。そのような時は、dispatch_retain
やdispatch_release
を用いてプログラマが管理する必要があります。
例えば、並列なキューを作って実行する場合のサンプルは次のようになります。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0 ; i < 100; ++i) {
dispatch_async(queue, ^{
NSLog(@"%d", i);
});
}
dispatch_release(queue);
なお、グローバルキューとメインキューに関してはシステムが管理しているのでプログラマが管理する必要はありません。
はじめに
-
導入
-
1.3 UIViewController1 UIViewController のカスタマイズ(xib, autoresizing)
-
UIKit 1 - container, rotate-
-
UIKit 2- UIView -
-
UIKit 3 - table view -
-
UIKit 4 - image and text -
-
ネットワーク処理
-
ローカルキャッシュと通知
-
Blocks, GCD
-
設計とデザインパターン
-
開発ツール
-
テスト
-
In-App Purchase
-
付録