マルチスレッド大好き

Win32のマルチスレッドで遊ぶための備忘録

基本
スレッドの同期
スレッドで遊ぶ
ガベージコレクション(GC)を使う
スレッドにメッセージを送信する その1
マルチスレッドでLameを実行する

基本

スレッドを作る関数は3つあります。 (CreateRemoteThread関数は別の機会に。)

_beginthread (CRT)

_beginthreadは簡単ですよ。 スレッドハンドルは_endthreadで閉じるので自分で閉じる必要はありません。

#include <process.h>

//スレッドを作るとき
//戻り値:成功=スレッドハンドル 失敗=1L
uintptr_t _beginthread(
    void( *start_address )( void * ),//スレッド関数のアドレス
    unsigned stack_size,//スタックサイズ。0でもよい
    void *arglist//スレッドに渡す引数、またはNULL
);

void 関数名(void *p);//スレッド関数の型

void _endthread( void );//スレッドが終わるとき

_beginthreadex (CRT)

_beginthreadexが一番安全らしい。 スレッドハンドルはCloseHandleを使って自分で閉めます。 このハンドルにExitThreadは使用禁止。

#include <process.h>

//スレッドを作るとき
//戻り値:成功=スレッドハンドル 失敗=0
uintptr_t _beginthreadex(
    void *security,//SECURITY_ATTRIBUTES 構造体へのポインタ またはNULL
    unsigned stack_size,//スタックサイズ。0でもよい
    unsigned ( *start_address )( void * ),//スレッド関数のアドレス
    void *arglist,//スレッドに渡す引数、またはNULL
    unsigned initflag,//0(すぐ実行) またはCREATE_SUSPENDED(一時停止)
    unsigned *thrdaddr//スレッド識別子。NULLでも可。
);

unsigned __stdcall 関数名(void *p);//スレッド関数の型

void _endthreadex( unsigned 戻り値 );//スレッドが終わるとき

CreateThread (Win32API)

CreateThreadは、_beginthreadexと引数は同じ。 でもスレッド関数内でランタイムライブラリ(asctimeとか)を使うとメモリリークするらしい。

#include <Windows.h>

//スレッドを作るとき
//戻り値:成功=スレッドハンドル 失敗=0
HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes,//SECURITY_ATTRIBUTES 構造体へのポインタ またはNULL
    DWORD dwStackSize,//スタックサイズ 0でもよい
    LPTHREAD_START_ROUTINE lpStartAddress, //スレッド関数のアドレス
    LPVOID lpParameter,//スレッドに渡す引数、またはNULL
    DWORD dwCreationFlags,//0(すぐ実行) またはCREATE_SUSPENDED(一時停止)
    LPDWORD lpThreadId//スレッド識別子。Win9x系でなければNULLでも可。
);

DWORD WINAPI 関数名(LPVOID p);//スレッド関数の型

VOID ExitThread(DWORD dwExitCode);//スレッドが終わるとき

戻り値

_endthreadex関数、ExitThread関数で指定した値を見るにはGetExitCodeThread関数を使う。 終了コードがSTILL_ACTIVEかどうかでスレッドが終わったかを見ることが一般的だったりするらしい。

//スレッドの終了コードを得る
//成功時0以外を、失敗は0を返す。
//スレッドが生きているとき、終了コードにSTILL_ACTIVE(0x103)が入る。
BOOL GetExitCodeThread(
  HANDLE hThread,      // スレッドハンドル
  LPDWORD lpExitCode   // 終了コード
);

スレッドの同期

シグナル状態と非シグナル状態

この用語は検索しても、難しい説明だったり、「シグナル状態とはシグナル状態だ!それ以外何者でも無い!!」といった説明ばかりですが、 早い話が待機関数(WaitFor〇〇関数)に引っかかるのが非シグナル状態です。 シグナル状態=青信号(ON)、非シグナル状態=赤信号(OFF)とでもしておきましょうか。

イベント

シグナル状態と非シグナル状態のどっちかしかありません。

自動リセットと手動リセットがありますが、待機関数(WaitFor〇〇関数)を抜けた後、勝手に非シグナル状態になるのが自動リセットです。 シグナル状態のままなのが手動リセットです。

//CreateEvent:イベントオブジェクトを作る。
イベントハンドル CreateEvent(
    セキュリティ(NULL可),
    手動リセット(TRUE)または自動リセット(FALSE),
    初期状態(TRUE=シグナル状態,FALSE=非シグナル状態),
    イベントの名前(NULLで名無し)
);

//OpenEvent : 名前のあるイベントを開きます。
イベントハンドル OpenEvent(
    アクセス権(EVENT_ALL_ACCESS=すべて,EVENT_MODIFY_STATE=イベントの変更のみ),
    CreateProcessで作ったプロセスにハンドルを継承可能にするか(TRUE / FALSE),
    イベントの名前
)

//SetEvent:イベントオブジェクトをシグナル状態にする。
SetEvent(イベントハンドル);

//ResetEvent:イベントオブジェクトを非シグナル状態にする
ResetEvent(イベントハンドル);

//CloseHandle : イベントハンドルを閉じる。
CloseHandle(イベントハンドル);

セマフォ

カウンタが0より大きければでシグナル状態、0なら非シグナル状態になります。 待機関数(WaitFor〇〇関数)を使えばカウンタが1つ減ります。

私の考え方:30人のイケメンが3台しかない自販機で順番待ちをしている感じ。
イケメン=スレッド、自販機=セマフォ、順番待ち=待機関数

//セマフォを作ります。
セマフォのハンドル CreateSemaphore(
    セキュリティ(NULL可),
    初期カウント(0以上、0なら非シグナル状態で開始。
    最大値(0より大きくすること),
    セマフォの名前(NULLで名無し)
);


BOOL ReleaseSemaphore(
  セマフォのハンドル,
  カウンタ増分(最大値を超える場合はカウンタ値はそのままでFALSEが返る)
  呼び出し前のカウンタ
);

クリティカルセクション

複数のスレッドが同じデータをいじるのを防ぎます。

関数の使い方はとても簡単です。 それぞれの関数にクリティカルセクション構造体へのポインタを入れるだけです。

私の考え方:30人のグラドルが1つしかない手鏡で順に髪をセットしている様子。
別解:セマフォの最大値が1になったもの。

//クリティカルセクションオブジェクトを初期化します。
InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

//クリティカルセクションに入ります。後から来たスレッドは待機状態になります。
//注意:同じスレッド内で立て続けに2回以上呼び出してもスルーされますよ。(デッドロック防止のためらしい)
EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

//クリティカルセクションから出ます。後のスレッドがクリティカルセクションに入ることができます。
LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

//クリティカルセクションオブジェクトを殺します。
DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

待機関数

時間切れやシグナル状態になるまで待機します。 WaitForSingleObjectは1つのオブジェクト、WaitForMultipleObjectsは複数のオブジェクトを待機できます。 待機可能なオブジェクトはMSDNによると、

DWORD WaitForSingleObject(オブジェクトハンドル,待つ時間。ミリ秒指定、またはINFINITE(ずーっと));

DWORD WaitForMultipleObjects(
    オブジェクトの数
    ハンドルの配列
    待ち条件フラグ(TRUE=すべてシグナル状態になるまで FALSE=どれか1かシグナル状態になるまで)
    待つ時間
);

戻り値は
WAIT_OBJECT_0 (0x00000000) オブジェクトハンドルがシグナル状態になったよ。
WAIT_ABANDONED (0x00000080) オブジェクトハンドルは放棄されたミューテックスオブジェクトでした。
WAIT_TIMEOUT (0x00000102) 時間切れ。
WAIT_FAILED (0xFFFFFFFF) 失敗。

スレッドで遊ぶ

_beginthreadexを使ったマルチスレッドプログラムです。

#include <stdio.h>
#include <process.h>
#include <windows.h>

unsigned __stdcall sure1(void *p)
{
    puts(p);
    _endthreadex(0);
    return 0;//コンパイラの警告を殺す
}

main()
{
    HANDLE handoru;
    
    handoru = (HANDLE)_beginthreadex(NULL, 0, sure1, "sure1です。", 0, NULL);
    WaitForSingleObject(handoru, INFINITE); /* スレッドが終了するまで待つ。 */
    CloseHandle(handoru); /* ハンドルを閉じる */
}

イベントを使ってみました。

/* スレッドの同期で遊ぶ イベント */
#include <stdio.h>
#include <process.h>
#include <windows.h>

#define namae "ORE_NO_IBENTO"

unsigned __stdcall kakikomi(void *p)
{
    FILE *fp;
    HANDLE ibento;
    
    _sleep(100);
    if ((fp = fopen("dogeza.txt", "w")) == NULL) goto owari;
    fprintf(fp, "今日もひきこもっています。そろそろ光合成が恋しくなってきました。\n");
    fclose(fp);
    
owari:;
    /* イベントをシグナル状態にする */
    ibento = OpenEvent(EVENT_ALL_ACCESS, FALSE, namae);
    SetEvent(ibento);
    CloseHandle(ibento);
    _endthreadex(0);
    return 0;
}

unsigned __stdcall yomikomi(void *p)
{
    FILE *fp;
    char buf[500];
    HANDLE ibento;
    
    /* イベントがシグナル状態になるまで待ちぼうけ */
    ibento = OpenEvent(EVENT_ALL_ACCESS, FALSE, namae);
    WaitForSingleObject(ibento, INFINITE);
    CloseHandle(ibento);

    if ((fp = fopen("dogeza.txt", "r")) == NULL) goto owari;
    fgets(buf, 500, fp);
    fclose(fp);
    printf("%s", buf);
    
owari:;
    _endthreadex(0);
    return 0;
}

main()
{
    HANDLE handoru[2];
    HANDLE ibento;
    
    /* 非シグナル状態のイベントを作る */
    ibento = CreateEvent(NULL, TRUE, FALSE, namae);
    
    handoru[0] = (HANDLE)_beginthreadex(NULL, 0, kakikomi, NULL, 0, NULL);
    handoru[1] = (HANDLE)_beginthreadex(NULL, 0, yomikomi, NULL, 0, NULL);
    
     /* スレッドが終了するまで待つ */
    WaitForMultipleObjects(2, handoru, TRUE, INFINITE);
    CloseHandle(handoru[0]);
    CloseHandle(handoru[1]);
    CloseHandle(ibento);/* イベントを閉じる */
    remove("dogeza.txt");
}

mesを2つのスレッド(sure1,sure2)で共有しています。 クリティカルセクションを使わないと sure1から出るときにmesがsure2で書き換えられてしまいます。

/* スレッドの同期で遊ぶ クリティカルセクション */
#include <stdio.h>
#include <process.h>
#include <windows.h>
CRITICAL_SECTION cs; /* クリティカルセクション構造体。触るな。 */

char mes[500];/* 共有リソースのつもり */

unsigned __stdcall sure1(void *p)
{
    EnterCriticalSection(&cs);
    strcpy(mes, "sure1");
    printf("%s に入りました。\n", mes);
    _sleep(200);/* 時間稼ぎ */
    printf("%s から卒業します。\n", mes);
    LeaveCriticalSection(&cs);
    _endthreadex(0);
    return 0;
}

unsigned __stdcall sure2(void *p)
{
    _sleep(100);/* 時間稼ぎ */
    EnterCriticalSection(&cs);
    strcpy(mes, "sure2");
    printf("%s に入りました。\n", mes);
    
    printf("%s から卒業します。\n", mes);
    LeaveCriticalSection(&cs);
    _endthreadex(0);
    return 0;
}

main()
{
    HANDLE handoru[2];
    
    InitializeCriticalSection(&cs);/* クリティカルセクション初期化! */
    handoru[0] = (HANDLE)_beginthreadex(NULL, 0, sure1, NULL, 0, NULL);
    handoru[1] = (HANDLE)_beginthreadex(NULL, 0, sure2, NULL, 0, NULL);
    
     /* スレッドが終了するまで待つ */
    WaitForMultipleObjects(2, handoru, TRUE, INFINITE);
    CloseHandle(handoru[0]);
    CloseHandle(handoru[1]);
    DeleteCriticalSection(&cs);/* クリティカルセクション終わり */
}

ガベージコレクション(GC)を使う

Boehm GCを使って、マルチスレッドプログラムをいじり倒します。 バージョン6.3からはスタティックライブラリでもマルチスレッドが扱えます。

README.win32に書いてあるように、 gc.hをインクルードする前にGC_WIN32_THREADSを定義します。 CreateThreadではなく、GC_CreateThreadを使います。(引数は同じ。) _beginthreadexとかは使ってはいけないようです。

#define GC_WIN32_THREADS
#include "gc.h"
#define CreateThread GC_CreateThread
#define malloc GC_MALLOC

//メモリ使用量を表示する
void mem(void)
{
    MEMORYSTATUS ms;
    
    ms.dwLength = sizeof(MEMORYSTATUS);
    GlobalMemoryStatus(&ms);
    printf("メモリ状況:%d%% (物理メモリ %dMB 仮想メモリ %dMB)使用中\n",
        ms.dwMemoryLoad, 
        (ms.dwTotalPhys-ms.dwAvailPhys) / (1024*1024),
        (ms.dwTotalPageFile-ms.dwAvailPageFile) / (1024*1024));
}

//スレッド関数
DWORD WINAPI sure1(LPVOID p)
{
    int i = 0, *ip;
    
    mem();
    puts("メモリリーク開始!");
    for (;i <= 100000; i++) {
        printf("%d (%d%%)\r", i, i*100/100000);
        ip = malloc(1024);
    }
    puts("\nメモリリーク終了!");
    mem();
    return TRUE;
}

main()
{
    HANDLE hsure;
    
    hsure = CreateThread(NULL, 0, sure1, NULL, 0, NULL);
    WaitForSingleObject(hsure, INFINITE);
    CloseHandle(hsure);
    puts("終わり");
}

スレッドにメッセージを送信する その1

/* スレッドにメッセージを送る その1*/

#include <windows.h>
#include <stdio.h>

#define WM_SURE_TEST1 (WM_APP + 1)
#define WM_SURE_TEST2 (WM_APP + 2)
#define WM_SURE_TEST3 (WM_APP + 3)

/* スレッド 引数pにイベントハンドル */
DWORD WINAPI oreore(LPVOID p)
{
    MSG msg;
    
    /* メッセージキューを作る。*/
    PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE);
    SetEvent((HANDLE)p);
    
    /* メッセージループ 
    WM_QUITを受け取るか、GetMessageが失敗したら抜ける
    */
    while (GetMessage(&msg, NULL,0,0) > 0) {
        switch (msg.message) {
        case WM_SURE_TEST1:
            puts("WM_SURE_TEST1");
            break;
        case WM_SURE_TEST2:
            puts("WM_SURE_TEST2");
            break;
        case WM_SURE_TEST3:
            puts("WM_SURE_TEST3");
            break;
        default:
            puts("理解できないメッセージです");
        }
    }
    ExitThread(0);
}


main()
{
    HANDLE hsure;
    DWORD id;
    HANDLE hevent;
    
    hevent = CreateEvent(NULL, TRUE, FALSE, "CUE_MADA");
    hsure = CreateThread(NULL ,0 , &oreore, hevent, 0, &id);
    if (hsure == NULL) return 0;
    
     /* スレッドにメッセージキューが出来るまで待つ */
    WaitForSingleObject(hevent, INFINITE);
    CloseHandle(hevent);

    /* メッセージを送る */
    PostThreadMessage(id, WM_SURE_TEST1, 0, 0);
    PostThreadMessage(id, WM_SURE_TEST2, 0, 0);
    PostThreadMessage(id, WM_SURE_TEST3, 0, 0);

    PostThreadMessage(id, WM_QUIT, 0, 0);
    WaitForSingleObject(hsure, INFINITE);
    CloseHandle(hsure);
}

マルチスレッドでLameを実行する

lameはシングルスレッドです。(version3.98.2現在)
いまやマルチコア全盛の世の中なのですが、lameはマルチスレッドにすると問題があったため いまだにシングルスレッドなのだそうです。

そこで私は、マルチプロセス動作するようにしたら、無駄がなくなるのではと思い、 実際に作ってみました。 スレッドを作って、その中でlameを呼び出すようにしています。 セマフォを使ってスレッド数の上限を決めています。

/*
lame マルチスレッドエンコーダ ver0.02
*/

#include <windows.h>
#include <stdio.h>
#include <process.h>

#define EVENT         "岸本セシルちゃんかわええええ!!!"
#define SEMAPHORE     "比留川游ちゃんかわえええええ!!!"

static char *my_lame_preset[] = {"-b 320 -h", "-V 2 -h", "-b 128", "-b 32"};
enum {                             B320=0,      V2,        B128,     B32, };

typedef struct {
    char infile[300];//ファイル名
    char *option;//オプション(lame_presetから選択)
    unsigned int ex;//付加情報など
} ENCODE_SETTING;


//CPUの個数を得る
static int getcpucount()
{
    SYSTEM_INFO si;
    
    GetSystemInfo(&si);
    if (si.dwNumberOfProcessors < 2) return 1;
    return si.dwNumberOfProcessors;
}


//エンターキー押されるまで待つ
static void wait_enterkey(void)
{
    char buf[8];
    
    fgets(buf, 5, stdin);
}


//エンコードスレッド
int __stdcall encode_thread(void *p)
{
    ENCODE_SETTING elst;
    char msg[1000];
    PROCESS_INFORMATION pi;
    static STARTUPINFO si;
    
    memcpy(&elst, (ENCODE_SETTING *)p, sizeof(ENCODE_SETTING));
    SetEvent(OpenEvent(EVENT_ALL_ACCESS, TRUE, EVENT));//値のコピー終了
    
    printf("%d/%d : %s (%s)\n", HIBYTE(elst.ex), LOBYTE(elst.ex), elst.infile, elst.option);
    sprintf(msg, "lame %s %s", elst.option, elst.infile);//lameに渡すコマンド
    memset(&si,0, sizeof(si));
    si.cb=sizeof(si);
    CreateProcess(NULL,(LPTSTR)msg,NULL,NULL,FALSE,NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW,NULL,NULL,&si,&pi);
    WaitForSingleObject(pi.hProcess,INFINITE);//プロセス終了まで待機
    printf("%d/%d : %s\n", HIBYTE(elst.ex), LOBYTE(elst.ex), "エンコード終わり");
    
    //スレッド終了
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    ReleaseSemaphore(OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE, SEMAPHORE), 1, NULL);
    _endthreadex(0);
    
    return 0;//コンパイラを黙らせるため
}


int main(int argc, const char *argv[])
{
    ENCODE_SETTING encset;
    uintptr_t *hthre;
    int max_thread, n;
    HANDLE ibento, semafo;
    DWORD tim;

    if (argc == 1) return 0;//引数なし
    
    //初期化
    tim = GetTickCount();
    max_thread = getcpucount();//最大スレッド数はCPU個数
    
    hthre = malloc(argc * sizeof(uintptr_t));
    ibento = CreateEvent(NULL, FALSE, TRUE, EVENT);
    semafo = CreateSemaphore(NULL, max_thread, max_thread, SEMAPHORE);
    
    printf( "マルチスレッドLAME ver0.02\n"
            "CPU=%d個 : 最大スレッド数=%d\n"
            "エンコードを開始します。\n\n"
            , getcpucount(), max_thread);
    
    for (n=1;n<argc;n++) {
        WaitForSingleObject(ibento, INFINITE);//スレッドに引数がコピーされるまで待つ
        strcpy(encset.infile, argv[n]);
        encset.option = my_lame_preset[V2];
        encset.ex = MAKEWORD(argc-1, n);//(L)現在のファイル/(H)最大ファイル数

        WaitForSingleObject(semafo , INFINITE);//スレッド上限
        hthre[n-1] = _beginthreadex(NULL, 0, encode_thread, &encset, 0, NULL);
    }
    WaitForMultipleObjects(n-1, (HANDLE)hthre, TRUE, INFINITE);//スレッド終了待ち
    
    //後始末
    for (n=1;n<argc;n++) CloseHandle((HANDLE)hthre[n-1]);
    CloseHandle(semafo);
    CloseHandle(ibento);
    free(hthre);
    
    printf("実行時間=%8.3f秒\n[ENTER]で終了します\n", (GetTickCount() - tim) / 1000.0);
    wait_enterkey();
    
    return 0;
}