Version 14.18
クリティカルセクションを使おう!
「前回は、グローバル変数にメインスレッドとサブスレッドからアクセスす
る例を見てみました」
『結構簡単にできたね』
「そこが危険」
『え”』
「 Version 14.01 ( No.268 ) でやったでしょ、同期を取らなきゃいけない
っていう話」
『そういえば、同期を取らないと……』
杵Aが叩く
杵Bが叩く
おもちをこねる
杵Bが叩く
杵Aが叩く
おもちをこねる
おもちをこねる
杵Aが手を叩く
『ってなっちゃうんだよね』
「そういうこと。実際にこれをプログラムで見てみます」
// 共有する変数。
int g_iData = 0;
// 別スレッドで呼び出される、 g_iData を増やし続ける関数。
void __cdecl IncrementThread( void *p_p )
{
TRACE( "スレッド開始。\n" );
for( int iF1 = 0; iF1 < 10; ++iF1 )
{
int i = g_iData;
// 0.1 秒待ちます。
Sleep( 100 );
g_iData = i + 1;
TRACE( "%d\n", g_iData );
}
TRACE( "スレッド終了。\n" );
}
// IncrementThread() を別スレッドとして呼び出す関数。
// この関数をボタンのイベントハンドラとかで呼んでください。
void CallThread()
{
HANDLE hThreadAry[3];
hThreadAry[0]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
hThreadAry[1]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
hThreadAry[2]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
WaitForMultipleObjects( 3, hThreadAry, TRUE, 100 * 1000 );
TRACE( "処理完了。\n" );
TRACE( "%d\n", g_iData );
}
「これは、 g_iData を増やし続ける IncrementThread() 関数を別スレッド
で3つ同時に実行する例」
『 10 回ループしてるんだね。 1 回につき 1 増えて、ループは 10 回、
スレッドは3つ、ってことは 30 になるってことだよね』
「ところがそうならないんです」
処理完了。
10
『あれ? 10 って……あ、これがさっきの同期取ってないってゆーのの問
題?』
「そういうこと。問題はここ」
int i = g_iData;
// 0.1 秒待ちます。
Sleep( 100 );
g_iData = i + 1;
「まず、 g_iData から i に値をコピーして、 0.1 秒待ってから、 1 増や
して、それをまた g_iData に入れます」
『それがまずいの?』
「3つのスレッドが同時にアクセスしちゃうからね。たとえばスレッド1が
g_iData から i にコピーしたとき、 i が 1 だったとします」
『うん』
「次に、このスレッド1が g_iData に i + 1 を入れる前に、スレッド2が
g_iData から i にコピーしたら?」
『 1 のまま……あ。スレッド1が g_iData に i + 1 を入れて、 g_iData
は 2 、だけど、スレッド2も同じ事をするから……』
「 g_iData はまた 2 が上書きされちゃうんです」
『げげげ』
「これを表にしてみると……」
g_iData スレッド1 スレッド2 スレッド3
1 i = g_iData
1 i : 1 i = g_iData
1 i : 1 i : 1 i = g_iData
2 ←←←←g_iData = i + 1 i : 1 i : 1
2 ←←←←←←←←←←←←←g_iData = i + 1 i : 1
2 ←←←←←←←←←←←←←←←←←←←←←←g_iData = i + 1
「という感じになります」
『あれ? そういえばこれってどっかで……』
「 Version 14.02 ( No.269 ) でしょ」
『あ、そうそう。そっか、あのときはファイルだったけど、今回はメモリの
中の話なんだね』
「そういうこと。同期とか排他処理とか、そういう観点では同じ。ファイル
でも変数でも〈取得・編集・書き込み〉っていう流れがある場合には、この
流れ全体を排他する必要があるわけです」
『そうしないと、編集してる間に他が取得して、結果が変わっちゃうわけ
ね……』
「そこで、今回は最後の同期オブジェクト【クリティカルセクション】を
使ってみます」
クリティカルセクション ( Critical Section )
『お、やっとだね』
「クリティカルセクションは他の同期オブジェクトとかなり性質が異なりま
す」
・名前を持たない
・ひとつのプロセスの中だけで機能する
・シグナルを使わない
・WaitForSingleObject() 系関数を使わない
『なんか全然別物って感じが……』
「そう、だから一番最後にしたっていうのもあるかな。でも、使いやすいか
も」
『使いやすい?』
「考え方は違うけど、ちょっと使ってみればわかると思うよ」
// 共有する変数。
int g_iData = 0;
// クリティカルセクション用同期オブジェクト。
CRITICAL_SECTION g_stCriticalSection;
// 別スレッドで呼び出される、 g_iData を増やし続ける関数。
void __cdecl IncrementThread( void *p_p )
{
TRACE( "スレッド開始。\n" );
for( int iF1 = 0; iF1 < 10; ++iF1 )
{
// クリティカルセクションでロックを掛けます。
EnterCriticalSection( &g_stCriticalSection );
int i = g_iData;
// 0.1 秒待ちます。
Sleep( 100 );
g_iData = i + 1;
TRACE( "%d\n", g_iData );
// クリティカルセクションのロックを解除します。
LeaveCriticalSection( &g_stCriticalSection );
}
TRACE( "スレッド終了。\n" );
}
// IncrementThread() を別スレッドとして呼び出す関数。
// この関数をボタンのイベントハンドラとかで呼んでください。
void CallThread()
{
// クリティカルセクションを初期化します。
InitializeCriticalSection( &g_stCriticalSection );
HANDLE hThreadAry[3];
hThreadAry[0]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
hThreadAry[1]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
hThreadAry[2]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
WaitForMultipleObjects( 3, hThreadAry, TRUE, 100 * 1000 );
TRACE( "処理完了。\n" );
TRACE( "%d\n", g_iData );
// クリティカルセクションを削除します。
DeleteCriticalSection( &g_stCriticalSection );
}
『あれ? 他の同期オブジェクトみたいに関数いっぱいないんだ、ってゆー
かさっきのプログラムと似てるね』
「さっきのプログラムの改善版だからね。変わったのは」
// クリティカルセクション用同期オブジェクト。
CRITICAL_SECTION g_stCriticalSection;
「が追加されて、このクリティカルセクション用同期オブジェクトを使う
4つの API を呼び出すようにした、っていうところかな。とりあえず試し
てみて」
『ほい。あ、ちゃんと 30 ってなった!!』
「というわけで次回に続く」
/*
Preview Next Story!
*/
『でも他の同期オブジェクトとぜんっぜん違うね』
「同期オブジェクトって考えない方がいいかも」
『じゃあ同期オブジェクトじゃないんじゃない』
「でも同期オブジェクトなんです」
『わけわかんない』
「というわけで次回」
< Version 14.19 クリティカルセクションを使ってみる! >
『につづく!』
「新しい気持ちで、でも他の同期オブジェクトのことは踏まえて」
『よけいわかんない』