結果に何が表示されるか分かりますか?
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
加数変数への増分値の原子加算を実行します。