Objective-Cで非同期処理を同期処理にする方法。

同期処理は簡単に非同期処理にできますが、非同期処理は簡単には同期処理にできません。

Objective-Cで、非同期処理を同期的に実行するにはどうしたら良いのかと調べたところ、ディスパッチセマフォを使うのが良いようでした。

セマフォとは信号装置とかいう意味らしいですが、「カウンタ」だと思って良いんじゃないかと思っています。

同期処理の例

まずは通常の同期処理から。

for (int i = 0; i < 5; i++) {
    sleep(1);
    NSLog(@"Process: %d", i);
}

これは、1秒ごとにカウントを表示する処理です。これは当然同期処理です。実行を始めると5秒間はこの先に処理が進めません。

同期処理を非同期処理にする

同期処理を非同期処理にするには、dispatch_asycを用いて、バックグラウンドのスレッドのキューに処理を追加すれば良いです。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    for (int i = 0; i < 5; i++) {
        sleep(1);
        NSLog(@"Process: %d", i);
    }
});

disptach_get_global_queueで非同期スレッドのキューを取得し、dispatch_asyncでそこに処理を追加します。

こうすると、この処理全体としては非同期処理になります。実行するとあっという間にこの処理を通過します。

非同期処理を同期処理にする

この例は、同期処理だったものを非同期処理にして、また同期処理に戻すものなので、意味のないものです。ですが、例としては悪くないと思います。

NSLog(@"start.");
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    for (int i = 0; i < 5; i++) {
        sleep(1);
        NSLog(@"Process: %d", i);
    }
    dispatch_semaphore_signal(semaphore);
});
 
NSLog(@"wait...");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
 
NSLog(@"finish.");

セマフォによって非同期処理の実行状態を待ちます。その完了を検知してから処理を先に進めることで、非同期処理を同期処理にすることができます。

実行結果

2012-12-01 19:24:41.046 AsyncToSync[55861:4703] start.
2012-12-01 19:24:41.049 AsyncToSync[55861:4703] wait...
2012-12-01 19:24:42.050 AsyncToSync[55861:6103] Process: 0
2012-12-01 19:24:43.052 AsyncToSync[55861:6103] Process: 1
2012-12-01 19:24:44.054 AsyncToSync[55861:6103] Process: 2
2012-12-01 19:24:45.057 AsyncToSync[55861:6103] Process: 3
2012-12-01 19:24:46.059 AsyncToSync[55861:6103] Process: 4
2012-12-01 19:24:46.060 AsyncToSync[55861:4703] finish.

ソースコード上は後ろにあるはずのwaitの方が、Processより先に出ています。したがって、非同期処理のブロックを一度は通過していることが分かります。その後、非同期処理が終わると待機処理を通過し、finishのログを出力します。

ディスパッチセマフォの使い方

ここでは、非同期処理を同期処理にするために、セマフォを使っています。

セマフォは、同時に実行される並列処理の数を制限するために用います。dispatch_semaphore_createの引数で指定する値は並列して実行する処理の最大数です。

今回は0を指定しているので少し不思議な感じですが、セマフォのカウントをあえてマイナスにして待つという手法を使っています。

dispatch_semaphore_waitを呼ぶと「セマフォのカウントを一つ減らす」ことができます。そして、その先の処理へ進みます。しかし、ディスパッチセマフォは「カウントがマイナスになるとそのスレッドを停止させる」という性質があります。だから、セマフォのカウントを-1にすることでメインの処理を留めておくことができます。

非同期処理が終了すると、dispatch_semaphore_signalで「セマフォのカウントを一つ増やす」ことができます。するとセマフォのカウントが0になり、マイナスの状態によるスレッドの停止から抜けて、その先へ進むことができるという具合です。

使いどころ

基本的に同期処理を非同期処理にするのは簡単なので、開発の際は基本的に同期的な実装をして、必要に応じてその同期処理を非同期化するのが良いかと思います。しかし、ライブラリなどでは、もともと非同期処理で実装されていて同期的に実行できない場合があります。それを「どうしても完了を待ちたい」となった場合に、このような手法を使うことができます。

ちなみに少しハマったんですが、セマフォがマイナスの間、そのスレッドが停止します。だから、非同期処理の中で、停止されたスレッドを使おうしても使うことができずフリーズしてしまいます。

About katty0324

2 comments

Leave a Reply

Scroll To Top