Grand Central Dispatch

最近、研究のPCのプログラムをiPhoneに移植することに挑戦していてマルチスレッドプログラミングを授業とかでやったが実践で使うのは初めてで、色々と勉強になったから 参考になったもののまとめとしてこの記事を書くことにした。
色々と忘れそうで...

以下GCDと呼ぶGrand Central DispatchはMacOS 10.6から使えてブロックを使いまくるライブラリです( 笑)WWDC2009に参加したときブロックの話が多くてイメージしていたが実際に使う機会がなくて やはり研究の様な重い処理や時間が掛かる様なものじゃないと使うまでもないし-[NSObject performSelectorOn...]NSOperationなどがあって低レベルのブロックを普段使うまでもありませんね。
WWDC2010ではGCDで盛り上がった.GCDはオプンソースになって現時点でMacOSとiOS以外に実装したOSはFreeBSDのみ

特に参考になったサイトは:
などなど


では、

インポート

まず、GCDはフレームワークではない、dylibなので
#import <dispatch/dispatch.h>
と書くだけでGCDのAPIが使える。

キューの作成

  • メインキュー : メインスレッドで実行
    dispatch_queue_t main = dispatch_get_main_queue();

  • グローバルキュー : バックグラウンドで実行
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    //priories:
    //DISPATCH_QUEUE_PRIORITY_HIGH
    //DISPATCH_QUEUE_PRIORITY_DEFAULT
    //DISPATCH_QUEUE_PRIORITY_LOW

  • プライベートキュー : バックグラウンドで実行、名前付きのキュー
    dispatch_queue_t queue = dispatch_queue_create("com.nacho4d.myApp.myQueue",NULL);
    デフォルトではプライオリティは DISPATCH_QUEUE_PRIORITY_DEFAULT だが変えることが出来る:
    dispatch_queue_t high = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,NULL);
    dispatch_set_target_queue(queue,high);
    

    他のタイプのキューとの大きな違いはブロックや関数が逐次的に実行されていくこと。他のキューではタスクをエンキューしても実際にどの順番で実行されるかが分からないが、プライベートなキューでは必ずキューの頭からタスクを実行していくのでシリアルなキューを実現できる。
    プライベートなキューを作成するとRetainカウントが1なので、 最後にリリースをすることを忘れずに
    dispatch_release(high);
    

キューの停止

停止できるが、実行中のブロックを止めることができません。停止がその次のブロックから適用される、つまりNSOperationQueueのsetSuspended:と全く同じ
dispatch_suspend(queue);

非同期と同期

実行するキューを指定し、ブロックを非同期的にまたは同期的に実行
dispatch_async(queue,^{/* 非同期 */});
dispatch_sync(queue,^{/* 同期 */});
例:
dispatch_queue_t queue = dispatch_queue_create(“com.app.task”,NULL)
dispatch_queue_t main = dispatch_get_main_queue();

dispatch_async(queue,^{
 CGFLoat num = [self doSomeMassiveComputation];

 dispatch_async(main,^{
  [self updateUIWithNumber:num];
 });
});
または
dispatch_queue_t queue = dispatch_queue_create(“com.app.task”,NULL);

__block CGFloat num = 0;

dispatch_sync(queue,^{
 num = [self doSomeMassiveComputation];
});

[self updateUIWithNumber:num];

ブロックを後で実行

-[NSObject performSelector:afterDelay]などのメソッドの代わりに使えるdispatch_after関数がある.利点:メソッドは不要

void dispatch_after(
   dispatch_time_t when,
   dispatch_queue_t queue,
   dispatch_block_t block);

例:0.3秒後にメインスレッドでブロックを実行する(UIKitのUITextViewを扱うのでメインスレッドに変えている).
dispatch_after(300, dispatch_get_main_queue(), ^{
        UITextView *textView = (UITextView *)[controller.view viewWithTag:12345];
        [textView removeFromSuperview];
        textView = nil;
    });

Forループの並列化

重い処理を分けて行って高速化を!
void dispatch_apply( size_t iterations, dispatch_queue_t queue, void (^block)(size_t));
例えば、CoreVideoのイメージバッファーの中から特定なチャネルだけコピーしたい場合は普段このように行うでしょう:
uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer); 
size_t width = CVPixelBufferGetWidth(imageBuffer); 
size_t height = CVPixelBufferGetHeight(imageBuffer); 

char *imageData = llahProcessor.inputImageDataBaseAddress;
int stepWidth = width*3; //stepWith of output image
for (int j = 0; j < height; j++) {
 for (int i = 0; i < width; i++) {
  int out_pixIndex = j*stepWidth + i*3;
  int in_pixIndex = j*bytesPerRow +i*4;  
  imageData[out_pixIndex]   = baseAddress[in_pixIndex]; //B
  imageData[out_pixIndex+1] = baseAddress[in_pixIndex+1]; //G
  imageData[out_pixIndex+2] = baseAddress[in_pixIndex+2]; //R
 }
}

しかし、dispatch_apply関数を用いて並列化を行うことができます:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_apply(height, queue, ^(size_t j) {
   for(int i = 0; i < width; ++i ) {
    int out_pixIndex = j*stepWidth + i*3;
    int in_pixIndex = j*bytesPerRow +i*4;
    imageData[out_pixIndex]   = baseAddress[in_pixIndex]; //B
    imageData[out_pixIndex+1] = baseAddress[in_pixIndex+1];//G
    imageData[out_pixIndex+2] = baseAddress[in_pixIndex+2];//R
   }
});
GCD並列処理を行っているので理論上では早いですが、例えば640x480の画像でforループの480個を同時に走らせとうしている!つまり実際に並列処理をするには480個のキューが必要だがそんな膨大な数のキューを作るには少し時間が掛かるしおそらく全部作れないのでエンキューされるforループのあるでしょうし 640x480の画像を処理を一瞬で終わるのでこの例は現実的ではないですえ。dispatch_apply関数の使い方だけに着目してください。マルチスレッドの話は難しくて長過ぎて教科書を読むが一番ですね

シングルトン

Colin Wheeler氏が指摘するようにブロックを使ったシングルトンの方がマルチスレッドセフティでその為に dispatch_onceを使います. dispatch_onceはアプリケーションが実行している間に一回のみ実行されると保証してくれる関数で便利ですね。
+(MyClass *)singleton {
 static dispatch_once_t pred;
 static MyClass *shared = nil;
 
 dispatch_once(&pred, ^{
  shared = [[MyClass alloc] init];
 });
 return shared;
}
まぁ〜 これでGCDのことをちょっと忘れたときにちらっと見れば思い出すかと ... うむ

怠け者の為のObjective-Cのメモリ管理

私はメモリ管理関連のの記事を沢山読います.特にiPhone開発初めてからリークなきクラッシュなきアプリの開発に心掛けた。(モバイルデバイスだから下手に扱うとすぐメモり不足の警告が出されたりするから)

そして、最近ツイッターでこの記事(英文)を見つけたので、簡潔で非常に分かりやすくて日本の方に是非読み頂ければという意思でこの記事を翻訳することにした.(簡潔で非常に分かりやすいですよ!)
私はネイティブではないので、おそらく間違っているところがあるだろうが、エッセンスを理解して頂ければと思う.(→最近友人に見てもらって色々と修正をしてくれたので大感謝!)



James Gregoryさんが想像しているよりObj-Cでメモリの管理は簡単

メモリ管理はということばを聞くとスクリプティングする若者たちとJava世界の人が怖がることを私は知っています。でも、実はそんな怖くないんだよ。詳細を言う前にJames Gregoryさんはどう思うかを見てみよう.


iPhoneにないの?!。マジ、アップルよ 一体なんだよ!? ガーベージコレックションがなぜそんなに凄いかと説明しようとするギークが沢山いるから、ここは短く言う。すばらしい理由が基本的に2つある.
一つ目、さらに他の言語のメモリのオーナーシップ(所有権)、いつリリースするベキかとかを理解する暇はないから.
二つ目、開発言語の特徴の中で 開発のプロセスを最も加速するガーベージコレックション(GC)の他に思い当たらないからだだ.
アンドロイドにはあるし、ハードウエアに直接関係しているプログラム以外(つまり、ほど全て)の場合は効率的だ。初代のiPhoneにはなかった理由が分かるかもしれないが 最近のiPhoneにない訳ないだろう。メモリ管理があまりにも大変でこれだけで初心者の開発者にiPhoneよりはアンドロイドを始めることを薦めると思うよ.



えっ!? それは違うよ。メモリ管理について怠け者たちが、大変だと誤解しているんじゃないかなぁ〜。

「さらに他の言語のメモリのオーナーシップ(所有権)、いつリリースするベキかとかを理解する暇はないから」というのは、デベロッパーがめんどくさいな と思っているだけじゃないかなぁ〜。

オブジェクト指向的なメモリ管理の基本は一緒。C++やらObjectPascalでもGCなしの言語なら みんな一緒だ. コンピュータサイエンスの基本の概念でObjective-Cに使うのはほぼささいなことだ.

もし、私は面接官で面接受けている人がそういうこと言ったら、その時点で面接が終了したなぁ、きっと.

しかし、私はGCがあるべきだと思うよ. GCの利点が知らなかったり、分っていないと勘違いしないでください。だから「手動のメモリ管理が大した苦労ではない」というときに あなたはそれを聞いた方がいいと思う。特にObjective-Cでは.

デスクトップのCocoaで開発する(私は結構書くなぁ)ときにCGを使うときがある. プログラムが少し大きくなって、ちょっと調べたり研究したりしているときにCGを使うことが多い。一旦自分が調べたことや試していたことが証明できたらCGを無効にして、全てをリファクタリングするから.

だから、時々GCを使うなら、いつも使えばいいじゃ〜. メモリの管理しなくてもいいから. それにプライベートフレームワークを作って再利用する傾向があるから、それらを動かす為にCGが無効になっているか有効になっているかを見なくてもよくなるし.

Objective-Cのretain/releaseの仕組みは 根本的なメモリ管理とGCの中間的な仕組みだから. C++とかと違って実際にオブジェクトをfreeするわけではない.代わりに「僕は このオブジェクトを使い終えたよ!」と言ってやって あとランライムに任せばいい.




基本の4つのルール

Objective-Cを学ぶときに覚えなければルールが4つだけ.
  • オーナーシップを持つなら、リリースするベキ
  • オーナーシップを持たないなら、リリースしちゃダメ
  • 自分のクラスの実装でdeallocをオバーライドし、オーナーシップを持ったものをリリースするベキ
  • deallocを直接呼び出すベキでない
それは全てだ. 上の2つが最も重要で オーナーシップの基本を次に説明する.

ルール1: オーナーシップを持つとき

Objective-Cでは オブジェクトをallocか、initか、newをしたらオーナーシップを持つころになる.例えば、


簡単だね、これより詳細な説明も思いつかないぐらい。しかし、確認の為に
allocしたらオーナーシップを持つことになる.
copyしたらオーナーシップを持つことになる.
newしたらオーナーシップを持つことになる.(newはalloc/initのショットカットだけ)
〜〜〜Jamesさん、もしまだ読んでいたら 既に4分の1を終えているから、もう少し読んだらソフト掛けるようになるゼ〜〜〜

ルール2:オーナーシップを持っていないとき

これはちょっとややこしいんだ. もっていないときはもっていないんだよ. だから、もしalloc/init/newいずれもしていなかったら 持っていないのだ. 次の例を見てみよう:

そのNSStringのオーナーシップを持っている?いいえ、alloc/copy/newもしなかったから持っていない.
そのNSImageは?はい、持っている.allocしたから.
そして、そのNSData? これも、いいえ. alloc/copy/newどれもしなかったからだ.
ちなみに、本当はこう直すベキ:

〜〜〜そうだね、GCより遥かに大変だわ! 普段ココアの開発者はどういうふうにやってんの?〜〜〜

ルール3と4: dealloc

これは一番大変なところだ. クラスの中にretainしているインスタンス変数があったら、オブジェクトがdeallocされるときにそれらのインスタンス変数をreleaseしないといけない.これを示すのは次の例:

上記の例では、SomeObjectは二つのインスタンス変数がある. thingssomeOtherThingsだ. initメソッドを見て分かるのは 二つをオブジェクトを生成して、それらをインスタンス変数にアサインしていること.
thingsに関しては [NSMutableArray arrayWithObjects]を使っているからretainを呼び出す必要がある. alloc/copy/newいずれもしない. autoreleaseしたオブジェクトを返すコンビニエンスメソッドを使ったから 明示的にretainをしなければいけない. autoreleaseしたオブジェクトとは何かと思っているかもしれないが それはその次に説明する. とりあえず、上記に説明したメモリ管理の基本だけを頭に入れておいてください.
deallocメソッドに 生成したオブジェクトをリリースしているだけです。それ以上はなにもありません.
(ちなみに、コンビニエンスメソッドとは:alloc/init/newで始まらないオブジェクトを生成してくれるメソッド。例: [NSArray array]はコンビニエンスメソッド、[[NSArray alloc] init]はコンビニエンスメソッドではない。ご存知の通り、コンビニエンスというのは便利の意味を持っていて、releaseしなくてもいいから便利という理由で名前持っているらしい. @nacho4d)


プローパーティ

Objective-C 2.0はプローパティーを提供してくれる.その詳細はここでは話さないが プローパーティのライフサイクルの中で混乱が生じやすいところがあるので 少しだけ触れたいと思う. ルールは2つのみ
プローパティーの属性はretainまたはcopyだったら deallocnilをセットするベキ.
initメソッドなどでプローパーティを初期化(init)しているならば autoreleaseにするベキ.
例を見てみよう.


titleプローパーティに関して NSStringのコンビニエンスメソッドを使って生成しているからautoreleaseしたオブジェクトが返ってきて 何もする必要がありません.
subtitleプローパティーに関して 私たち、自分で、オブジェクトを新しく(alloc/initを使って)生成しているのでautoreleaseをする必要があります.
理由はこれから説明します. alloc/initを使ってretainカウントが1になり、subtitleにアサインすると一個増えるので2になってしまう. autorelease無しで行うとリテインカウントが決して1に戻らなず メモリリークが発生してしまう. だからautoreleaseを入れることで リテインカウントが1個減らしている.

オートリリースとは?

実は完全に嘘をついた、完全に.これは実は結構複雑な仕組みで その複雑さはオートリリースプール(Autorelease pool)という形で来ている。それは一体なんだ? オートリリースプールはreleaseメッセージを送られる対象オブジェクトのリストを持っている。いつ送られるかというとプール
は破壊される(destroyed)また排出させた(drained)ときです。 しかし、それはいつ起きるかを気にしなくても大丈夫です. 正しくautoreleaseメッセージを送っておければ オブジェクトがちゃんとリリースされるからだ.

さて、基本のルールに従って行えば オブジェクトを返すメソッドについての疑問がある筈だなぁ。このオブジェクトのオーナーシップを誰が持っているの?ここはオートリリースプールの出番だ.
例えば:

そのNSImageのオーナーシップを誰が持っている? ここはねぇ、この例では、メッセージを送ったオブジェクトはオーナーシップを持つことになる.よって送られた側にreleaseメッセージを送る必要がある。しかし、これは悪い例です.正しいのは次のよう:

Autoreleaseメッセージが最後にあることをに着目しよう.autoreleaseをすると そのオブジェクトがオートリリースプールに追加され poolが破壊されるときに releaseメッセージが送られます.
(なぜ一個上のが悪い例でこれは良い例かという説明を次にします。
実はメモリ管理の観点から見ると両方とも問題はありません。後でreleaseするか、さきにautoreleaseをするかどっちも一緒です。
しかし、前者の方がCocoaのコンベンションの一つに違反しています。 
私は上に書いたコンビニエンスメソッドの定義によると、alloc/init/newで始まるメソッドを送るとオーナーシップを持つことになります。
逆に言えば、そうで始まらないメソッドを送るとオーナーシップを持たないことになりますね。そして、getAnImageは何かの形でオブジェクトを生成しています。かつ、alloc/init/newで始まらないから コンビニエンスメソッドですよね?こう言ったコンビニエンスメソッドが CocoaのコンベンションによるとAutoreleasedオブジェクトを返す筈だよね?、だから コンベンションに従って書くようにしましょう. @nacho4d)

オートリリースプールの中にさらに別のオートリリースプールはあってもいいので、大量の一時的なオブジェクトを生成するときに便利. そして、もしマルチスレッドの処理をしていれば、新しいスレッド用のオートリリースプールを作る必要があります.

まとめ

思ったより、そんな怖くないし、複雑ではなかろう. 勿論GCを全然簡単だが、例えGCがあっても手動メモリ管理に向いている処理が沢山があって、手動メモリ管理がメーディアやゲームなどのアプリケーションでもっとも使われている。
このようなアプリケーションにはメモリ管理は極めて重要な役割で、知らないとうまくいかない場合が沢山あるよ. ありがたいことに CocoaとObjective-Cを使うとそんなに苦労しなくて済む.

参考文献

実はアップルのドキュメンテーションで十分な筈だが、すばらしい記事が沢山かあって...
などなど...

This work is licensed under BSD Zero Clause License | nacho4d ®