shimada-kの日記

ソフトウェア・エンジニアのブログです

GCDの実行ポリシーとキューの種類まとめ

今更ながらGCDを勉強しました。Grand Central Dispatchです。わからなければ自分で使ってみることです。そうしたらいざという時に役に立ちます。

調査

GCDには主に2種類のキューのタイプと2種類のディスパッチポリシーがあります。組み合わせで合計4種類の使い方が存在します。

  1. 非同期実行&並列キュー
  2. 非同期実行&直列キュー
  3. 同期実行&並列キュー
  4. 同期実行&直列キュー

webの資料を読んでもいまいちイメージがつかなかったので以下のようなコードで4パターン検証してみました

環境

#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
#include <dispatch/dispatch.h>

#define CEIL_MAX    100000
#define NUM_THREAD  20
#define CONTEXT_PRINT   1 

// 1からceilまでの間で素数の個数を計算する関数
int prime(int ceil, int thread_order)
{
    int i, j, num_yakusu, prime = 0;

    for(i = 1; i <= ceil; i++){
        num_yakusu=0;

        for(j = 1; j <= i; j++){
            if(i % j == 0){
                num_yakusu++;
            }
        }
        
        // 1とそれ自身でしか割り切れなかったら素数
        if(num_yakusu == 2){
            prime++;
            if(CONTEXT_PRINT){
                printf("thread order:%d\n", thread_order);
            }
        }
    }
    return prime;
}

// entry point
int main(int argc, char *argv[]){
    int i;

    //dispatch_queue_t q = dispatch_queue_create("gcd.sample.osx", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t q = dispatch_queue_create("gcd.sample.osx", DISPATCH_QUEUE_SERIAL);
    //dispatch_group_t group = dispatch_group_create();

    srand((unsigned)time(NULL));
    printf("loop start\n");

    for(i = 0; i < NUM_THREAD; i++){
        dispatch_group_async(group, q,
        //dispatch_sync(q,
            ^{
                pthread_t p = pthread_self();
                struct timeval tp;
                int ceil = rand() % CEIL_MAX + 1;
                int num_prime = prime(ceil, i);
                int j = i;
                struct timeval tq;

                printf("i:%d, thread id:%p ceil:%d num_prime:%d\n", i, p, ceil, num_prime);    
            }
        );
        printf("thread done\n");
    }
    //dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    printf("loop end\n");
}

1からceilまでの数値で素数がいくつあるか調べる処理をGCDを使って並列処理するコードです。

GCDの実行ポリシーとキューの種類の4パターンでソースコードを切り替え、生成されるスレッドの個数とディスパッチの対象を調べました。

パターン スレッドの個数 ディスパッチの対象
1 5 インキューされているすべてのタスク
2 2 実行中のタスクの次にインキューされたタスク
3 1 実行中のタスクの次にインキューされたタスク
4 1 実行中のタスクの次にインキューされたタスク

考察

パターン1と2は非同期実行です。1の場合は非同期実行&並列キューです。タスク同士が完全に独立で並列に実行されます。スレッドはメインスレッドの他に4スレッド生成されて、タスクすべてがコンテキストスイッチされて実行されます。

2の場合は非同期実行&直列キューです。メインスレッドの他に1つスレッドが生成されてそのスレッド内でインキューされた順番に実行されていきます。パターン1と2のケースは両方とも親スレッドとは別個にスレッドが作成されています。これは非同期実行ポリシーの特徴と言えます。

パターン3と4は同期実行です。3の場合は同期実行&並列キューです。並列キューなので複数スレッドが立ち上がると見せかけて実際起動するのは1スレッドだけです。

dispatch_syncはそれぞれのブロックの実行が完了するまで終わらないのです。なので、並列キュー的には複数スレッドを立てたいけど、処理できるブロックが1個ずつだから結果として1個しかスレッドが 立たないということになります。

4の場合は同期実行&直列キューです。これは言わずもがな純度100%の直列&同期実行です。インキューされた順番に1つ1つタスクが完了するまで次のタスクは実行されません。パターン3と4のケースは子スレッドが作成されるわけではありませんので親スレッドはブロックされます。

まとめ

実行結果より、キューの種類よりも実行ポリシーのほうが支配的であると言えます。

また「同期」と「直列」という単語が2種類出てくるのでわかりにくくなっているのですが、「同期」というのはタスク間に対して使われているものではなく、メインスレッドとの関係性を表す単語です。

そして「直列」というのはタスクの順番のみを表す単語として使用されています。少なくともGCDにおいては。

親スレッドとは別なところでCPUを全て使って複数処理を同時に走らせる(いちばんイメージしやすいパターン)なら並列&非同期の組み合わせ一択。親スレッドは自由にしたいというだけの場合は直列&非同期でも可。

むしろ上記以外のケースだった場合、GCD使ってもメリット薄い気がしますね。。

sched_setaffinityを使って非同期&並列キュー環境で全て同じCPUに固定した場合どうなるのか試したかったんですが、OSXでは使えませんでした。残念。

久しぶりにC言語を使いました。楽しかったです。