並列アプリケーションを作ってみよう 2ページ

 さて、並列処理を実装するにあたり、もっともプリミティブな方法がスレッドを利用した実装である。Windowsの場合、新しいスレッドを作成する方法にはいくつかあるが、Cでスレッドを作成する場合は_beginthreadex関数を使用するのが基本となる。

 _beginthreadex関数は「process.h」内で定義されており、そのプロトタイプは下記のようになっている。

uintptr_t _beginthreadex(
   void *security,
   unsigned stack_size,
   unsigned ( *start_address )( void * ),
   void *arglist,
   unsigned initflag,
   unsigned *thrdaddr
);

 詳細についてはライブラリリファレンス等を参照してほしいが、基本的にはstart_address引数に新たなスレッドで実行したい関数を、arglistに関数に引数として与える変数もしくは配列のポインタを与えて_beginthreadex()を呼び出せば良い。それ以外の引数については、特に問題がなければ0もしくはNULLで構わない。

 さて、今回のプログラムではループを使って各画素に対するメディアン処理を繰り返し行っている。このように繰り返し回数が多いループを含むプログラムの場合、このループを分割して並列処理させるのが定石である(図4)。

図4 繰り返し処理を並列化する定石
図4 繰り返し処理を並列化する定石

 まず、並列化したい個所を別の関数として分離し、この関数を別スレッドで実行することで並列処理を行うわけだ。この関数を別スレッドで実行する場合、関数には1つの引数しか与えられないので、複数の引数を与える場合はあらかじめそれを構造体として宣言しておき、その構造体のポインタを引数として与える。

 今回のサンプルプログラムで並列実行したい個所は次の部分である。この部分は最大で4重のループとなっているが、このうちもっとも外側のループ(「for(y = 1; y < height – 1; y++)」の部分)を分割することを考えよう。つまり、このループを「for(y = 1; y < (height – 1)/2; y++)」と「for(y = (height – 1)/2; y < height – 1; y++)」という2つのループに分割し、それぞれを並列に処理する、という流れである(並列化後のソースコード全文は記事末にリスト2として掲載)。

    /* フィルタを適用 */
    for(y = 1; y < height - 1; y++) {
        for (x = 1; x < width - 1; x++) {
            /* 対象ピクセルとその近傍8ピクセルのアドレスをソートバッファにコピー*/
            for (j = 0; j < 3; j++) {
                for (i = 0; i < 3; i++) {
                    tmp_array[3*j + i] = &(buf_tmp->array[y-1+j][x-1+i]);
                }
            }
            /* 9ピクセルをソート */
            med_sort(tmp_array, 9);
            /* 中央値となるピクセルの相対位置を取得 */
            dy = get_med_position_y(tmp_array, &(buf_tmp->array[y][x]));
            dx = get_med_position_x(tmp_array, &(buf_tmp->array[y][x]), width, dy);

            /* 出力バッファの対象ピクセルの値を中央値となるピクセルの値に設定 */
            for (i = 0; i < 3; i++) {
                buf_out->array[y][3 * x + i] = buf_in->array[y+dy][3 * (x+dx) + i];
            }
        }
    }

 この処理内ではいくつかの変数が使われているが、この処理を別の関数にまとめる際に引数として与える必要があるものはbuf_tmp、buf_in、buf_outの3つである。そこで、この3つの変数に、ループを開始する値(y_start)および終了する値(y_end)を加えた5つの変数を関数に与える配列として宣言する。そのほかの変数についてはすべてこのループ内で初期化され、また出力にも影響しないので関数内で確保を行えば良い。

typedef struct {
    image_buffer* buf_in;
    image_buffer* buf_out;
    image_buffer* buf_tmp;
    int y_start;
    int y_end;
} thread_param;

 この構造体のポインタを引数として受け取り、並列に処理を実行する関数は次のようになる。なお、_beginthreadexの引数に与える関数は__stdcall形式でなければならない点に注意しよう。

void __stdcall work_thread(thread_param* param) {
    int x, y;
    int dx, dy;
    int i, j;
    JSAMPLE* tmp_array[9];
    const image_buffer* buf_in = param->buf_in;
    const image_buffer* buf_out = param->buf_out;
    const image_buffer* buf_tmp = param->buf_tmp;
    const int y_start = param->y_start;
    const int y_end = param->y_end;
    const int width = param->buf_in->width;

    /* フィルタを適用 */
    for(y = y_start; y < y_end; y++) {
        for (x = 1; x < width - 1; x++) {
            /* 対象ピクセルとその近傍8ピクセルのアドレスをソートバッファにコピー*/
            for (j = 0; j < 3; j++) {
                for (i = 0; i < 3; i++) {
                    tmp_array[3*j + i] = &(buf_tmp->array[y-1+j][x-1+i]);
                }
            }
            /* 9ピクセルをソート */
            med_sort(tmp_array, 9);
            /* 中央値となるピクセルの相対位置を取得 */
            dy = get_med_position_y(tmp_array, &(buf_tmp->array[y][x]));
            dx = get_med_position_x(tmp_array, &(buf_tmp->array[y][x]), width, dy);

            /* 出力バッファの対象ピクセルの値を中央値となるピクセルの値に設定 */
            for (i = 0; i < 3; i++) {
                buf_out->array[y][3*x + i] = buf_in->array[y+dy][3*(x+dx) + i];
            }
        }
    }
}

 実際にスレッドを生成し、関数を呼び出す個所は次のようになる。

    /* 与える引数を設定 */
    for (i = 0; i < 2; i++) {
        param[i].buf_in = buf_in;
        param[i].buf_out = buf_out;
        param[i].buf_tmp = buf_tmp;
    }
    param[0].y_start = 1;
    param[0].y_end = (height - 1) / 2;
    param[1].y_start = (height - 1) / 2;
    param[1].y_end = height - 1;

    /* スレッド生成 */
    thread = (HANDLE)_beginthreadex(NULL, 0, (void*)work_thread, &(param[0]), 0, NULL);
    work_thread(&(param[1]));

    /* スレッドの終了を待つ */
    WaitForSingleObject(thread, INFINITE);
    CloseHandle(thread);

 ここでは_beginthreadexでy=1からy=(height-1)/2までの処理を行う別スレッド(スレーブスレッド)を起動させた後、もともとあったスレッド(マスタースレッド)でy=(height-1)/2からy=height-1までの処理を実行する