BBAsyncTask その4

非同期どころかexecuteInBackgound:はメインスレッドで動作してしまうのでexecuteInBackgound:の意味が無い。
次回はこの肝心要の非同期処理=別スレッドでの処理実行と他の処理との同期を実装していこう。

Objective-C、iOS上でのスレッドによる非同期処理はいくつかの世代があるが、今回はGrand Central Dispatch(以降GCD)による物を使うことにした。

理由は簡単でGCDは「最も新しく」「最も効果的で」「最も簡単に」マルチスレッド処理を記述できるからだ。

マルチスレッドによる非同期処理をいろいろな言語で書いて来たが、スレッドが一本増えるとそれだけでプログラミングは途端に面倒になるし処理効率も下がってしまう。それはスレッドという流れがブロックしない処理を扱うことは勿論だが、もう一つGUIアプリケーションの場合非同期処理が実行されるスレッドではGUI処理は実行できない、つまりは非同期処理といえどもGUIアプリケーションである以上、メインスレッド(UIスレッド)との同期も意識しなくはならないからだ。

GCDはMac OS X 10.6 Snow Leopardから導入されたメカニズムであり、マルチコアのMPUを最大限に生かすと共にそのプログラミングの負担を軽減する。GCDの詳細を説明すると本が一冊書けてしまう程の量が必要なのと、今回の処理ではGCDのほんの人握りの機能しか使わないので、ここでは割愛する。

さて、能書きはこの辺でとっととコードを紹介しよう。非同期処理を実行するaSyncExecuteメソッドの内容だ。

BBAsyncTask.m
- (void)aSyncExecute:(NSInvocation*)invocation
{
    __block NSInvocation* inv = invocation;
    
    // preExecute 実行
    SEL pre = @selector(preExecute:);
    if ( pre && [[self targetObject] respondsToSelector:pre] )
    {
        [inv setTarget:[self targetObject]];
        [inv setSelector:pre];
        [inv invoke]; 
        [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantPast]];
    }
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
 
    @try
    {
        dispatch_group_async(group, queue, 
        ^{
            //execute実行
            SEL exec = @selector(executeInBackgound:);
            [inv setTarget:[self targetObject]];
            [inv setSelector:exec];
            [inv invoke]; 
            [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantPast]];
        });
        
        dispatch_group_notify(group, queue, 
        ^{
            //postExecute 実行
            SEL post = @selector(postExecute:);
            if (post && [[self targetObject] respondsToSelector:post])
            {
                __autoreleasing id returnValue;
                [inv getReturnValue:&returnValue];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                [[self targetObject] performSelector:post withObject:returnValue];
#pragma clang diagnostic pop
                [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantPast]];
            }
        });
        
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    }
    @finally
    {
        dispatch_release(group);
    }
}
@end

ポイントは幾つかあるが、まずはGCDの非同期実行のためのコード。

キューを取得してグループを生成する
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();

アプリケーションから見て並列に実行できるキューは一つだけである。その一つしか無いグローバルキューをdispatch_get_global_queueマクロで取得する。
それとは別にキューで実行するグループをdispatch_group_createで生成するが、このグループは非同期処理を実現するだけであれば不要である。わざわざ生成するのは処理の待ち合せのためにグループが必要だからだ。

BBAsyncTaskのメソッドは記述の有無にもよるが、全て記述されたいた場合は以下の順で実行することが保証されなければならない。
1 - (void)preExecute:(id)param;
2 - (id)executeInBackgound:(id)param;
3 - (void)postExecute:(id)executeResult;

preExecute:は単に最初に呼び出せば良いので同期の必要は無いが、executeInBackgoundは既に書いたように別スレッドで「非同期に」実行されるため、処理が終了するまでブロックしない。従ってpostExecute:メソッドはexecuteInBackgoundの終了を待たずに実行されてしまうのである。

このように呼び出しの順序と終了の順序が一致しないのでマルチスレッドによる処理の特徴だが、他のスレッドで処理を効率よく動かすと共に、後処理である postExecuteを確実に呼び出すために待ち合せが必要になる。その待ち合せの単位がグループだ。

グループをキュー上で非同期に実行する
        dispatch_group_async(group, queue, 
        ^{
            //execute実行
            SEL exec = @selector(executeInBackgound:);
            [inv setTarget:[self targetObject]];
            [inv setSelector:exec];
            [inv invoke]; 
            [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantPast]];
        });

先ほど生成したグループを単位としてキューに処理するコードを渡している。キューに渡すコードは^{〜}の間に記述するが、これは既に判るようにObjective-CのBlocksによるラムダ式の構文となっている。

これでセレクタexecuteInBackgound:は別スレッドでキューの優先度に従って実行される。(生成時にはDISPATCH_QUEUE_PRIORITY_DEFAULTつまりデフォルトして指定されていた)

なお、既に書いたがdispatch_group_asyncマクロは即座に復帰する。

キューで実行された処理を待ち合せる
        dispatch_group_notify(group, queue, 
        ^{
            //postExecute 実行
            SEL post = @selector(postExecute:);
            if (post && [[self targetObject] respondsToSelector:post])
            {
                __autoreleasing id returnValue;
                [inv getReturnValue:&returnValue];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                [[self targetObject] performSelector:post withObject:returnValue];
#pragma clang diagnostic pop
                [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantPast]];
            }
        });
        
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

最後に実行しなくてはならないセレクタpostExecute:だが、コードをブロックに閉じ込めてdispatch_group_notifyに渡すことでグループの処理が終わった際に呼ばれる。このブロック自体も即座に復帰するが、すぐには実行されない。

dispatch_group_waitマクロはグループの処理が終わるまで(DISPATCH_TIME_FOREVER)待ち合せる。グループの処理すなわちdispatch_group_asyncに渡されたコードブロックの実行が完了することで通知グループに通知されて、ついにpostExecute:セレクタが実行される訳だ。

なお、それぞれの処理ブロックの際に書かれているコード

[[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantPast]];

これはメインスレッドのランループ(イベントループ)を一瞬だけ強制的に実行する処理である。まるで古のVBのDoEventのデジャブを見ているようだが、理屈や狙いは同じだ。

実はこの処理にはプログレスバーやHUDといったユーザに処理状況をフィードバックするためのGUIを配置しようと思っているのだが、他のスレッドで忙しく処理を実行している間にGUIを更新しても全く反映されない。なぜならばランループの処理でGUIの再描画が実施されないからだ。上の一行はこれを解消するためにランループを一瞬強制的に動作させて滞留しているUIスレッドの処理を進める訳だ。 (やっぱりVBのDoEventsじゃん)

これで非同期且つ、前処理と後処理の処理順が確保されてGUIの更新も行うことのできるメソッドとなった(テストはまだだが)

最後になるが

dispatch_release(group)

グループは有限のリソースのため、使用が終わったら必ず解放する必要がある。(ARCの影響を受けないことに注意)


それにしてもGCDで書く非同期処理は直感的で非常に分かり易い。

今までのオブジェクト指向型言語ではスレッドはThread自身をクラスとして抽象化することで複雑さを軽減してきた。GCDの語彙はその逆に、オブジェクト指向には関わらず敢えて低レベルなマクロとして記述するようになっている。しかし、このお陰でGCDの命令は記述するコンテキストを選ばず、クラスのインスタンスメソッド、クラスメソッド、はたまたCの関数上でも書くことが出来るのだ。

ブロックで必要なコードを囲こむことだけでコードは非同期に別スレッドで動き出すのである。言語はObjective-Cだがオブジェクト指向を盲信しない素晴らしい設計と実装だと思う。