GCDとpthraedのコード比較

Grand Central Dispathでで並列プログラミングする場合に、条件変数(Condition Variable)を使いたい時はどうすればいいのかみたいな話を同僚としてたので調べてみた。結論としてはpthread_cond_xxxを使えばOK。

ところで私はpthreadもGCDもそんなに詳しくないので、学習がてら小さいコードを書いて両者を比較してみることにした。

pthreadで並列プログラミング

まずはGCDを使わずに、pthreadを使ってマルチスレッドなコードを実装。適当にpthreadでぐぐったら、codezine良い感じの記事とサンプルコードがあったので、これをベースにする(一部いじってある)

引用元:CodeZine「pthreadについて(条件変数・モデル)」
http://codezine.jp/article/detail/1894

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#define THREAD_MAX 256

void* tmp_func(void* arg);

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t ready_mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t ready_cond = PTHREAD_COND_INITIALIZER;

int start_flg = 0; //(2)pthread_cond_broadcast受け取り漏れ対策

int main(int argc, char** argv) {
    pthread_t pt[THREAD_MAX];
    int thr_count = 10;
    
    pthread_mutex_lock(&ready_mutex);
    for (int i = 0; i < thr_count; i++) {
        pthread_create(&pt[i], 0, tmp_func, &i);  //(1)indexの受け渡し対策
        pthread_cond_wait(&ready_cond, &ready_mutex);
    }
    pthread_mutex_unlock(&ready_mutex);
    
    sleep(1);
    
    start_flg = 1;
    fprintf(stdout, "pthread_cond_broadcast\n");
    pthread_cond_broadcast(&cond);
    for (i = 0; i < thr_count; i++) {
        pthread_join(pt[i], 0);
    }
    return 0;
}

void* tmp_func(void* arg) {
    pthread_mutex_lock(&ready_mutex); //(3)signal発行前に呼び出し元でwaitしていることを担保
    int index = *(int*)arg;
    pthread_cond_signal(&ready_cond);
    pthread_mutex_unlock(&ready_mutex);
    
    fprintf(stdout, "start thread index:[%d]\n", index);
    pthread_mutex_lock(&mutex);
    
    while (start_flg == 0) {
        pthread_cond_wait(&cond, &mutex);
    }

    pthread_mutex_unlock(&mutex);
    fprintf(stdout, "end thread index:[%d]\n", index);
    return 0;
}

このコードは以下の様なことをやっている。

  • 10個のスレッドを起動する
  • 各スレッドには起動時にindexが与えられる
  • 各スレッドは条件変数により待機状態になる
  • 全てのスレッドが起動しきったところで、待機しているスレッドを起こす
  • 全てのスレッドの実行が完了したらプログラム終了

これだけだが、そのままだといくつか問題があるので以下のように対処している。

  • (1)でスレッド側でiのポインタを渡す際に、呼び出し元でiの値を書き変えてしまわないように、条件変数ready_condを使ってガード
  • 最後に生成されるスレッドがwaitする前に、pthread_cond_broadcastされると、broadcastを受け取ることなくwaitしつづけるので、(2)のstart_flg変数でwaitするしないを制御
  • スレッド側で変数iを受けとってpthread_cond_signalを発行する際に、呼び出し元でwaitしていることを担保するために(3)でready_mutexをロック

ややこしいですねぇ。

GCDで並列プログラミング

上記プログラムを、GCDを使って書き換えた結果がこちら。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <dispatch/dispatch.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;

int start_flg = 0; //(c)

int main( int argc, char ** argv ) {
    int thr_count = 10;
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    for (int i = 0; i < thr_count; i ++ ) {
        dispatch_group_async(group, queue, ^{
            int  index = i; //(b)
            fprintf( stdout, "start thread index:[%d] input!!\n", index );
            pthread_mutex_lock( &mutex );
            while (start_flg == 0) {
                pthread_cond_wait( &cond, &mutex );  //(d)
            }
            pthread_mutex_unlock( &mutex ) ;
            fprintf( stdout, "end thread index:[%d] output!!\n", index );
        });
    }
    sleep( 1 );

    start_flg = 1;
    pthread_cond_broadcast( &cond );
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER); //(a)
    dispatch_release(group);
    return 0;
}

なんかだいぶスッキリした気がします。

ポイントは以下。

  • まずはblocksで無名関数がかけるのでスッキリ
  • joinのかわりにdispath_group_waitを使って、待ち合わせ処理がスッキリ (a)
  • 変数iの値は(b)の時点でblocksによってキャプチャされるので、iの受け渡しのために条件変数と同期変数を使う必要がなくなってスッキリ
  • start_flgが必要なのはかわらず(c)
  • GCDを使っていてもpthread_condやmutexは問題なく使用可能(d)

GCDとblocksによってかなりスッキリして良い感じです。GCDによってスレッドの制御も最適化されて性能的にも良さげです。

このへんの話は以下の資料、書籍も参考になります。

並列プログラミングガイド
https://developer.apple.com/jp/devcenter/ios/library/documentation/ConcurrencyProgrammingGuide.pdf

エキスパートObjective-Cプログラミング ― iOS/OS Xのメモリ管理とマルチスレッド
http://tatsu-zine.com/books/objc

怪しい点があればご指摘ください。

2012/07/01 21:01追記

  • start_flgやめてdispatch_semaphoreをカウントダウンラッチ的に使えばいいね