2010年07月22日

スレッドの同期

突然ですが、こんなソースがあったとしたら、
結果に何が表示されるか分かりますか?
volatile int g = 0; // メモリから読むことを強制

WORD WINAPI tp( LPVOID v ) {
    for( int i = 0; i < 10000; i ++ ) g++;
    return 0;
}

int WINAPI WinMain(HINSTANCE hi, HINSTANCE hpi, LPSTR command, int show) {
    const int tmax = 64;
    HANDLE th[ tmax ];
    for( int i = 0; i < tmax; i ++ ) {
        DWORD tid;
        th[ i ] = CreateThread( 0, 0, (LPTHREAD_START_ROUTINE)tp, 0, 0, &tid );
    }
    // 終了待ち
    WaitForMultipleObjects( tmax, th, TRUE, INFINITE );

    // 止めてgを確認
    __asm int 3;
    return 0;
}

tpはgを10000回インクリメントするだけ。
WinMainはgを実行するスレッドを64個作るだけです。

やってみれば分かりますが、答えは「環境によって不定」です。
マルチスレッドを知らない人は「640000」だと思ったんじゃないでしょうか。

VC++2008付属コンパイラだと、これを
mov eax, 0x2710 // 10進で10000
mov ecx, 1
loop:
add [g], ecx
sub eax, ecx
jne loop
ret

こんな感じでコンパイルしてくれますが、
マルチコアCPUだとgへのaddが同時に起こることがあります。
その場合1回のaddと同じ結果になるので、
競合したaddの結果が失われることになり、結果としてインクリメントが失敗します。
つまり正確な答えは、「マルチコア/CPUの場合640000以下、それ以外は640000」
となります。

さて、これをアトミック(重複せずに、1つ1つ、等という意味)に処理しなければ
正常な結果は得られません。これを同期処理と言います。

同期にはミューテックス、セマフォなどなどありまして、
MicrosoftがWin32APIとしていろいろ提供しています。
その中でもとても単純で応用力のあるAPIが
InterlockedExchangeという関数。以下MSDNより。

InterlockedExchange
指定された 1 個の変数の内容ともう 1 つの値の交換を一括して行います。
この関数は、複数のスレッドが同じ変数を同時に使うことを防止します。

LONG InterlockedExchange(
LPLONG Target, // 交換に使われる変数
LONG Value // 新しい値
);
戻り値:Target パラメータが指す変数の、交換前の値が返ります。


この関数は同時に実行されないことが保証されているので、
こんな感じで同期処理が取れます。
long flag = 0;
WORD WINAPI tp( LPVOID v ) {
    for( ;; ) {
        if( InterlockedExchange( &flag, 1 ) == 0 ) break;
        Sleep( 1 );
    }

    for( int i = 0; i < 10000; i ++ ) g++;
    InterlockedExchange( &flag, 0 );
    return 0;
}

例としてスレッド1,2がtpを実行するとし、
スレッド2が先にInterlockedExchangeに入ったとします。

1.スレッド1はInterlockedExchangeで待機状態になります。
2.スレッド2がInterlockedExchangeから戻り値0を取得し、処理続行します。
3.スレッド1がInterlockedExchangeから戻り値1を受け取ります。
4.0でなかったので、一瞬眠って再度InterlockedExchangeを試行します。
 ※)スレッド2がflagを0にするまで3,4を繰り返します。
5.スレッド2がflagを0にします
6.スレッド1がInterlockedExchangeから戻り値0を取得し、処理続行します。

とまあこんな流れになります。ややこしいですけど。

ミューテックスにしろセマフォにしろ、結局は
同時に処理してはいけない状況への対策なので、
上記処理を応用すれば実現できます。たぶん。

余談)
類似関数としてInterlockedExchangeAddってAPIがあるんですが、
MSDNでの説明が酷いんですよね。以下MSDNより。

InterlockedExchangeAdd
加数変数への増分値の原子加算を実行します。
これで意味が分かった人はエスパーだと思います。
posted by Nick at 13:59| Comment(0) | TrackBack(0) | プログラミング
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/39759047

この記事へのトラックバック