logo7

 Allergy Design Office > マルチスレッドプログラミング

マルチスレッドプログラミング

 このページではマルチスレッド、プロセス間通信、スレッド間通信およびタイマープログラミングについて紹介します。(いやー。好きだなこういう話題は。)

 記述例およびサンプルに含まれるファイルの全部、または一部を使用したことによる損害等について、一切の責任を負いません。また、サンプルの文字コードはS-JISで提供しますので、ご使用の際はWindowsからFTPするなどして適切な文字コードに変換してください。尚、サンプル中には説明の簡略化のため意味のないコードや、実用上問題のあるコードも含まれていますのでご注意ください。

  1. [POSIX]pthread_create
  2. [POSIX]スレッドの同期(ミューテックス)
  3. [POSIX]タイマー
  4. [WIN32]_beginthreadex
  5. [WIN32]スレッドの同期(ミューテックス)
  6. [WIN32]共有メモリ
  7. [WIN32]スレッドの同期(イベント)

[POSIX]pthread

 「スレッドとは何であるか」とか「スレッドとプロセスの違いは何か」等の解説は他で語り尽くされているので詳しくはそちらに譲る。簡単に言うとプロセスでは互いに異なるアドレス空間を持ち、スレッドはアドレス空間を共有する。また、プロセスはスレッドの入れ物に過ぎない。

 スレッドを使用するメリットはプロセスの場合に比し、同期のために共有メモリ等の比較的コストのかかる方法を取らなくて良く、スレッド間の通信もグローバルな変数を使用すればよいだけであることにある。スレッドのスイッチングは比較的短時間で済む。

 スレッドの生成はpthread_createを呼び出す。元々あったスレッドをプライマリスレッド、新たに生成したスレッドをセカンダリスレッドとすれば、そのまま何もしないとプライマリスレッドは終了してしまうので、終了させたくない場合はpthread_join等を用いて新たに生成したスレッドの終了を待つ。

 以下の例では新たに生成したスレッドの終了をpthread_joinで待機している。pthread_joinはtidで識別されるスレッドが終了すると戻る。

    pthread_t   tid;
    int param;

                                            /*  受信スレッド生成            */
    pthread_create(&tid,NULL,waitReceiveThread,(void*)param);
    pthread_join(tid,NULL);                 /*  受信スレッド終了待ち        */
 スレッドルーチンはこのような感じとなる。
void*
waitReceiveThread(void* pParam)
{
    int param = (int)pParam;

    何らかの処理

    return NULL;
}
 スレッドには1つだけパラメータ(void*)を渡すことができる。本当にポインタ変数として使用する場合、それが指示するメモリ領域が問題になる。あるスレッドが生起したとき、そのスレッドを生成した(pthread_createを呼び出した)関数は、既に無効になっているかも知れない。

 pthread_createは、スレッドを生成してから戻るわけではなく、システムにスレッド生成を依頼するだけである。これはスレッドに渡すパラメータが指示するメモリ領域に、auto変数を使用できないことを意味する。さもなければ、そのauto変数が指示していた領域(スタック上にある)は既に別なことに使用されていてプログラムの破壊を招く。

<誤ったパラメータの渡し方>

    char    buf[128];                       /*  ローカル変数                */

                                            /*  スレッドが生起した時点で    */
                                            /*        bufは破棄されている   */
                                            /*          可能性がある        */
    pthread_create(&tid,NULL,waitReceiveThread,buf);

 ただし、ポインタ変数自体はコピーであるで、単一の変数の値を渡す場合、前述の例のようにvoid*にキャストすれば、auto変数を使用することができる。

POSIXスレッドのサンプル

[POSIX]スレッドの同期(ミューテックス)

 複数のスレッドで処理を行う場合、同期は避けて通ることができない。POSIXにはいくつかの同期を取る仕組みがある。ここでは最も簡単(と僕は思うのだが...)なミューテックスを使用してみることにする。

 例えば、以下のfunc1とfunc2はスレッドルーチンで、これらは見かけ上同時に実行される。このためそれぞれのスレッドのテキストは必ずしもシングルスレッドの時のように1行に出力されずに、他方のスレッドのテキストが割り込む。尚、printfの間にusleepを入れているのは、マシンの速度に左右されずにミューテックスあり及びなしのときの結果を確認しやすくするためのものであり、ミューテックスの処理とは関係がない。

void*
func1(void* pParam)
{
    int i;

    for(i = 0;i < 10;i++)
    {
        printf("output");
        usleep(0);
        printf(" by");
        usleep(0);
        printf(" func1 i = %d\n",i);
    }

    return NULL;
}

void*
func2(void* pParam)
{
    int i;

    for(i = 0;i < 10;i++)
    {
        printf("OUTPUT");
        usleep(0);
        printf(" BY");
        usleep(0);
        printf(" FUNC2 I = %d\n",i);
    }

    return NULL;
}
 これを1連の単語が1行に表示されるようにするためにミューテックスを使用して排他する。まず最初にpthread_mutex_initで使用するミューテックスの初期化を行う必要がある。そして排他が必要な処理に入る前にpthread_mutex_lockでミューテックスを取得する。排他が必要な処理が終了したらpthread_mutex_unlockでミューテックスを解放する。これだけである。

 ミューテックスは同時には1つしか取得できないため、一方のスレッドがミューテックスを獲得している間は、他方のスレッドはミューテックスを取得することができず、pthread_mutex_lockから戻ってこない。このため、他方のスレッドは例えCPUが空いていたとしてもそれ以降の処理を継続することができない。

 排他が必要な処理が終わると一方のスレッドはpthread_mutex_unlockでミューテックスを解放する。他方のスレッドはミューテックスを獲得し、pthread_mutex_lockから戻って以降の処理を継続することができる。

 まずはミューテックスを初期化する。

    pthread_mutex_init(&mutex,NULL);        /*  ミューテックス初期化        */
 次にfunc1のprintfをpthread_mutex_lockとpthread_mutex_unlockで挟むよう変更してみよう。func2も同様に変更する。
        pthread_mutex_lock(&mutex);         /*  ミューテックス取得          */
        printf("output");
        usleep(0);
        printf(" by");
        usleep(0);
        printf(" func1 i = %d\n",i);
        pthread_mutex_unlock(&mutex);       /*  ミューテックス解放          */
 これを実行するとfunc1及びfunc2のテキストは必ず1行ずつ出力されるようになる。

 最後にpthread_mutex_destroyでミューテックスを破棄する。ただし、現在のLinuxの実装では意味がないようであるので、なくても良いだろう。

    pthread_mutex_destroy(&mutex);          /*  ミューテックス破棄          */

ミューテックスのサンプル  注 マルチスレッドのプログラムはマシンの速度や、OSの処理(種類)によって挙動が変わる。ループの回数を増やす等すると現象が観測しやすくなる。ミューテックスの箇所をコメントアウトする等して動きを観測してみて欲しい。

[POSIX]タイマー

 タイマーの作成にはtimer_createを使用する。Linuxもカーネル2.4からPOSIX準拠のtimer_createが使用できるようになったようだ。POSIXのタイマーは満了するとそれを作成したプロセスにシグナルを発行する。つまり、そのシグナルに対するハンドラを登録することでタイマー処理が可能となる。シグナルは特に指定しない限りSIGALRMである。

<ハンドラの登録>

    struct sigaction    sigact;


    sigact.sa_handler = handler;
    sigact.sa_flags = 0;
    sigemptyset(&sigact.sa_mask);

                                            /*  ハンドラ設定                */
    if(sigaction(SIGALRM,&sigact,NULL) == -1)
    {
        perror("sigaction");
    }
 ハンドラ内ではとりあえずprintf等を記述してハンドラが呼ばれたことを確かめる。実際にはprintfをハンドラ内で使用すると問題があるかもしれないが、ここでは動作を見るために使用するだけなので問題ないであろう。これについてはOSに付属するプログラミングマニュアル等を見て確認して欲しい。
void
handler(int signo)
{
    printf("signo = %d\n",signo);
}
 次にタイマーの作成と設定を行う。ここでは2秒周期で起動するタイマーを作成してみる。timer_createでタイマーを作成しタイマーIDが得られたら、それをもとにtimer_settimeでタイマーを設定する。ここでitimerspec構造体のit_valueは最初のタイマー満了までの時間、it_intervalはそれ以後の周期である。ワンショットタイマーを作成する場合はit_valueを設定し、it_intervalは0を設定する。
    timer_t timerId;
    struct itimerspec   itval;


    itval.it_interval.tv_sec = 2;
    itval.it_interval.tv_nsec = 0;
    itval.it_value.tv_sec = 2;
    itval.it_value.tv_nsec = 0;

                                            /*  タイマー作成                */
    if(timer_create(CLOCK_REALTIME,NULL,&timerId) == -1)
    {
        perror("timer_create");
    }
                                            /*  タイマー設定                */
    if(timer_settime(timerId,0,&itval,NULL) == -1)
    {
        perror("timer_settime");
    }
 あとはpause等を使用してシグナルを待つ。
    while(1)
    {
        pause(0);                           /*  シグナルを待つ              */
    }
 必要がなくなったらtimer_deleteでタイマーを削除する。
                                            /*  タイマー削除                */
    if(timer_delete(timerId) == -1)
    {
        perror("timer_delete");
    }

POSIXタイマーのサンプル  僕はredhat linux 7.1FTP版で確認したが、複雑なことはしていないのでSunやHP等でもインクルードするヘッダの修正やリンクするライブラリの変更で動作すると思われる。

[WIN32]_beginthreadex

 Win32でもスレッドが使用できる。ただし、スレッドモデルはあまりスマートではない。僕はpthreadからこの世界に入ったので、ハンドルで待機したり、そのハンドルをクローズしなければならなかったりと結構戸惑った。(ハンドルを持つものは何でも、WaitFor系の関数で待つことができるので、それが必ずしも悪いとはいえないが。)

 通常、Win32のスレッドはCreateThreadを使用するが、スレッド内でCのライブラリを使用する場合は_beginthreadexを使用する。POSIXのときと同様そのまま何もしないとプライマリスレッドは終了してしまうので、以下の例では生成したスレッドの終了をWaitForSingleObjectで待機している。POSIXのコードと比較してみて欲しい。Win32の場合は使い終わったハンドルのクローズも忘れてはならない。

    HANDLE  hThread;
    DWORD   dwThreadId;
    int nParam;

                                            /*  受信スレッド生成            */
    hThread = (HANDLE)_beginthreadex(NULL,0,waitReceiveThread,(LPVOID)nParam,0,&dwThreadId);

    WaitForSingleObject(hThread,INFINITE);  /*  受信スレッド終了待ち        */
    CloseHandle(hThread);                   /*  ハンドルクローズ            */
 スレッドルーチンはこのような感じとなる。
DWORD WINAPI
waitReceiveThread(LPVOID    pParam)
{
    int nParam = (int)pParam;

    何らかの処理

    return 0;
}
 スレッドには1つだけパラメータ(LPVOID)を渡すことができる。変数の扱いについてはPOSIXのスレッドと同様なのでそちらも参照して欲しい。

<誤ったパラメータの渡し方>

    char    buf[128];                       /*  ローカル変数                */

                                            /*  スレッドが生起した時点で    */
                                            /*        bufは破棄されている   */
                                            /*          可能性がある        */
    hThread = (HANDLE)_beginthreadex(NULL,0,waitReceiveThread,buf,0,&dwThreadId);

 ただし、POSIX同様にポインタ変数自体はコピーであるで、単一の変数の値を渡す場合、前述の例のようにLPVOIDにキャストすれば、auto変数を使用することができる。

[WIN32]スレッドの同期(ミューテックス)

 Win32には様々な同期オブジェクトがあり、最初はどれを使えばいいのか迷ってしまうかもしれない。もっとも、細かな機能や速度の違いこそあれ、大きく変わるものではない。POSIX同様、ここでは最も簡単(と僕は思うのだが...)なミューテックスを使ってみることにする。

 例えば、以下のfunc1とfunc2はスレッドルーチンで、これらは見かけ上同時に実行される。このためそれぞれのスレッドのテキストは必ずしもシングルスレッドの時のように1行に出力されずに、他方のスレッドのテキストが割り込む。尚、printfの間にSleepを入れているのは、マシンの速度に左右されずにミューテックスあり及びなしのときの結果を確認しやすくするためのものであり、ミューテックスの処理とは関係がない。

DWORD WINAPI
func1(LPVOID    pParam)
{
    int i;

    for(i = 0;i < 10;i++)
    {
        printf("output");
        Sleep(0);
        printf(" by");
        Sleep(0);
        printf(" func1 i = %d\n",i);
        Sleep(0);
    }

    return 0;
}

DWORD WINAPI
func2(LPVOID    pParam)
{
    int i;

    for(i = 0;i < 10;i++)
    {
        printf("OUTPUT");
        Sleep(0);
        printf(" BY");
        Sleep(0);
        printf(" FUNC2 I = %d\n",i);
        Sleep(0);
    }

    return 0;
}
 これを1連の単語が1行に表示されるようにするためにミューテックスを使用して排他する。まず最初にCreateMutexでミューテックスオブジェクトの作成を行う必要がある。そして排他が必要な処理に入る前に待機関数のWaitForSingleObjectでミューテックスを取得する。排他が必要な処理が終了したらReleaseMutexでミューテックスを解放する。これだけである。

 ミューテックスは同時には1つしか取得できないため、一方のスレッドがミューテックスを獲得している間は、他方のスレッドはミューテックスを取得することができず、WaitForSingleObjectから戻ってこない。このため、他方のスレッドは例えCPUが空いていたとしてもそれ以降の処理を継続することができない。

 排他が必要な処理が終わると一方のスレッドはReleaseMutexでミューテックスを解放する。他方のスレッドはミューテックスを獲得し、WaitForSingleObjectから戻って以降の処理を継続することができる。

 まずはミューテックスを作成する。

    hMutex = CreateMutex(NULL,FALSE,NULL);  /*  ミューテックス生成          */
 次にfunc1のprintfをWaitForSingleObjectとReleaseMutexで挟むよう変更してみよう。func2も同様に変更する。
                                            /*  ミューテックス取得          */
        WaitForSingleObject(hMutex,INFINITE);
        printf("output");
        Sleep(0);
        printf(" by");
        Sleep(0);
        printf(" func1 i = %d\n",i);
        Sleep(0);
        ReleaseMutex(hMutex);               /*  ミューテックス解放          */
 これを実行するとfunc1及びfunc2のテキストは必ず1行ずつ出力されるようになる。

 不要になったらCloseHandleでミューテックスを破棄する。

    CloseHandle(hMutex);                    /*  ハンドルクローズ            */

ミューテックスのサンプル  注 マルチスレッドのプログラムはマシンの速度や、OSの処理(種類)によって挙動が変わる。ループの回数を増やす等すると現象が観測しやすくなる。ミューテックスの箇所をコメントアウトする等して動きを観測してみて欲しい。

[WIN32]共有メモリ

 通常、Win32ではプロセス毎に別のメモリ空間が与えられ、2つの異なるプロセス同士は互いのメモリ空間にアクセスすることはできない。共有メモリは2つ(またはそれ以上)のプロセスからのアクセスを可能にするメモリ領域で、これらのプロセス間の通信に使用することができる。

 共有メモリは先ずサーバ側でCreateFileMappingによってファイルマッピングを作成し、MapViewOfFileでサーバ側プロセスのアドレス空間にマップしてそのポインタを得る。このとき、CreateFileMappingに指定するlpNameが、他プロセスからこの共有メモリを識別するためのキーとなる。

                                            /*  共有メモリ作成              */
    hFileMapping = CreateFileMapping((HANDLE)0xffffffff,
        NULL,
        PAGE_READWRITE,
        0,
        sizeof(SOMEDATA),
        "share1");

    pData = (SOMEDATA *)MapViewOfFile(hFileMapping,
        FILE_MAP_ALL_ACCESS,
        0,
        0,
        0);
 クライアント側でも同様のことを行うが、今度はCreateFileMappingではなくOpenFileMappingを使う。このとき、OpenFileMappingのlpNameにはサーバ側でCreateしたときと同じものを指定する。するとOpenFileMappingはこの名前で識別されるファイルマッピングのハンドルを返す。サーバ側と同様クライアント側のアドレス空間に共有メモリをマップし、そのポインタを得る。
    hFileMapping = OpenFileMapping(FILE_MAP_ALL_ACCESS,
        TRUE,
        "share1");

    pData = (SOMEDATA *)MapViewOfFile(hFileMapping,
        FILE_MAP_ALL_ACCESS,
        0,
        0,
        0);
 サーバ及びクライアントのプロセスで共有メモリが見えるようになったら、取得したポインタを通して自由にデータの交換を行うことができまる。アプリケーションからは通常のメモリ操作と何ら変わりない。
    pData->a = 100;                         /*  int値設定                   */
    pData->b = 99.9;                        /*  double値設定                */
    strcpy(pData->c,"allergy");             /*  文字列設定                  */
 共有メモリが不要になったらUnmapViewOfFileで共有メモリをアンマップし、CloseHandleでファイルマッピングのハンドルを解放します。この処理はサーバ及びクライアントで同一である。
    UnmapViewOfFile(pData);
    CloseHandle(hFileMapping);

[WIN32]スレッドの同期(イベント)

 イベントは何か起こるのをCPU時間を消費せずに待つことができるある大変便利な仕組みであり、Winsockではこの仕組みを用いてデータの受信待ちを行う事ができる。イベントを使用すれば例えばスレッドの外部からそのスレッドの終了を制御することができる。

 まずはイベントを作成する。

	HANDLE	m_hEvent;

	m_hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);	// 自動リセットイベント作成
 作成時には手動リセットか自動リセットかを選択できる。自動リセットにすると待機しているスレッドが解放された時点で自動的に非シグナル状態となる。つまり、WaitForから抜けた後、わざわざResetEventする必要がない。特に必要がなければ自動リセットが便利だろう。

 また、最初からシグナル状態で作成することもできるので状況に応じて使い分ける。後は何か起こるのをそのイベントのハンドルで、既に何度も出てきているWaitForSingleObject等で待てば良い。

    WaitForSingleObject(m_hEvent,INFINITE);         // イベント待ち
 何か起こす側は何か起こると同時にイベントをSetEventでシグナル状態にする。
    SetEvent(m_hEvent);                             // イベントをセット

 不要になったらCloseHandleでイベントを破棄する。

    CloseHandle(m_hEvent);                          // ハンドルクローズ

Copyright(C) 2001-2003 Allergy Design Office All rights reserved.

[Home]