Version 14.20
クリティカルセクションと同期オブジェクト
「今回は、さらにクリティカルセクションの使い方について見てみます」
『へー、他の同期オブジェクトなんて2回ずつしかやらなかったのに』
「それだけクリティカルセクションは使いやすいし、使う場面も多いから
ね。それに、使うのが難しいっていうのもあるかな」
『難しい?』
「まず、前回の例を思い出して。前回は g_iData という変数を各スレッド
で共通で使用していたから、この変数にアクセスする箇所を排他するために
クリティカルセクションを使用しました」
『読み込んで、書き込むところを囲んだわけよね』
「ということは、同じく g_iData にアクセスする箇所があった場合、同じ
ように囲まなきゃいけないってことです」
『?』
「たとえば」
// 別スレッドで呼び出される、 g_iData を増やし続ける関数。
void __cdecl IncrementThreadOld( 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" );
}
『あれ、クリティカルセクション使ってない、ってゆーか前のだね』
「これを混在させたら?」
HANDLE hThreadAry[3];
hThreadAry[0]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
hThreadAry[1]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
hThreadAry[2]
= (HANDLE)_beginthread( IncrementThreadOld, 0, NULL );
『え”、それって意味ないんじゃない?』
「そう、意味がないんです。 g_iData にアクセスする箇所はすべて同期を
取らないと意味がないんです。でも……」
『でも?』
「 g_iData はグローバル変数なので、色々な関数からアクセスできます。
もしプログラムが大きくて、色々な関数から g_iData にアクセスしていたら
……」
『……全部クリティカルセクションで囲まなきゃいけないってこと? うわ
めんどくさー』
「そういうこと。たとえばこんな感じに」
// 別スレッドで呼び出される、 g_iData を増やし続ける関数。
void __cdecl IncrementThreadOld( 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" );
}
『なるほど、同じように囲めばいいわけね』
「同期オブジェクト、つまり同じ g_stCriticalSection を使えば、両方と
も同期を取ってくれるから」
『?』
「えっと、たぶん感覚的にはわかってるんだろうけど……この例を試してみ
て」
// 共有する変数。
int g_iData = 0;
// クリティカルセクション用同期オブジェクト。
CRITICAL_SECTION g_stCriticalSection;
// クリティカルセクション用同期オブジェクト(Old用)。
CRITICAL_SECTION g_stCriticalSectionOld;
// 別スレッドで呼び出される、 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" );
}
// 別スレッドで呼び出される、 g_iData を増やし続ける関数。
void __cdecl IncrementThreadOld( void *p_p )
{
TRACE( "スレッド開始。\n" );
for( int iF1 = 0; iF1 < 10; ++iF1 )
{
// クリティカルセクションでロックを掛けます。
EnterCriticalSection( &g_stCriticalSectionOld );
int i = g_iData;
// 0.1 秒待ちます。
Sleep( 100 );
g_iData = i + 1;
TRACE( "%d\n", g_iData );
// クリティカルセクションのロックを解除します。
LeaveCriticalSection( &g_stCriticalSectionOld );
}
TRACE( "スレッド終了。\n" );
}
// IncrementThread() を別スレッドとして呼び出す関数。
// この関数をボタンのイベントハンドラとかで呼んでください。
void CallThread()
{
// クリティカルセクションを初期化します。
InitializeCriticalSection( &g_stCriticalSection );
InitializeCriticalSection( &g_stCriticalSectionOld );
HANDLE hThreadAry[3];
hThreadAry[0]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
hThreadAry[1]
= (HANDLE)_beginthread( IncrementThread, 0, NULL );
hThreadAry[2]
= (HANDLE)_beginthread( IncrementThreadOld, 0, NULL );
WaitForMultipleObjects( 3, hThreadAry, TRUE, 100 * 1000 );
TRACE( "処理完了。\n" );
TRACE( "%d\n", g_iData );
// クリティカルセクションを削除します。
DeleteCriticalSection( &g_stCriticalSection );
DeleteCriticalSection( &g_stCriticalSectionOld );
}
『クリティカルセクションの同期オブジェクトがふたつある』
「 IncrementThread() と IncrementThreadOld() 用、別々に用意しまし
た。すると……」
『あれ? 結果が 20 になっちゃった』
「これは、 IncrementThread() と IncrementThreadOld() が別々に同期を
取っているから。たとえば」
void __cdecl IncrementThread( void *p_p )
{
...
// クリティカルセクションでロックを掛けます。
EnterCriticalSection( &g_stCriticalSection );
// ※↓スレッド1進行中↓
// g_stCriticalSection にロックがかかりました。
「 IncrementThread() がロックを掛けている状態の時、」
void __cdecl IncrementThreadOld( void *p_p )
{
...
// ※↓スレッド2進行中↓
// クリティカルセクションでロックを掛けます。
EnterCriticalSection( &g_stCriticalSectionOld );
「このとき、 g_stCriticalSectionOld は g_stCriticalSection と全く関
係がないので、 g_stCriticalSection にセットされている〈ロック中〉情
報がありません」
『ってことは……』
「排他されずに」
void __cdecl IncrementThreadOld( void *p_p )
{
...
// クリティカルセクションでロックを掛けます。
EnterCriticalSection( &g_stCriticalSectionOld );
// ※↓スレッド2進行中↓
// g_stCriticalSectionOld にロックがかかりました。
『げ、これじゃ意味ないじゃん』
「対して、同じ変数を使っていた場合」
// ※↓スレッド2進行中↓
// ロックがかかっているので待機します。
// クリティカルセクションでロックを掛けます。
EnterCriticalSection( &g_stCriticalSection );
「同じ g_stCriticalSection を参照すれば、スレッド1が掛けたロックを
スレッド2もちゃんと見てくれます」
『これなら関数が増えても問題なしってわけね』
「それはそうなんだけど、 g_iData にアクセスする箇所がどんどん増えて
いったら?」
『う”』
「そのうえ、 g_iData から取得するところと g_iData に書き込むところが
別々になったりしたら」
『うわ……考えたくないし、そういうプログラム作らなきゃいいんで
しょ?』
「それができたらねぇ……」
/*
Preview Next Story!
*/
『なんかちゃんと同期を取るのって大変そう』
「マルチスレッドが難しい理由のひとつだね」
『しかも複雑になればなるほど……』
「しかも、クリティカルセクションでもデッドロックが発生します」
『げ』
「というわけで次回」
< Version 14.21 スレッドでデッドロック >
『につづく!』
「クリティカルセクションもちゃんとした同期オブジェクトってことで」
『デッドロックが起こせることが〈ちゃんと〉なの?』