Version 14.22
複数の同期オブジェクト
「今回は、まず前回の復習から」
『クリティカルセクションでデッドロック、だよね』
「クリティカルセクションに限らず、デッドロックが発生する条件っていう
のがあります」
・同期オブジェクト1、2が存在する。
・スレッド1、2が存在する。
・スレッド1は同期オブジェクト1にロックを掛けている間に
同期オブジェクト2を待機する。
・スレッド2は同期オブジェクト2にロックを掛けている間に
同期オブジェクト1を待機する。
「この条件だと、確率的にデッドロックが発生する可能性があります」
『確率的?』
「そう、この条件だと、デッドロックが発生するのは」
・スレッド1が同期オブジェクト1にロックを掛けて、
同期オブジェクト2の待機を開始する前に、
スレッド2が同期オブジェクト2のロックを掛ける。
「という場合。前回の例で言うと」
// 別スレッドで呼び出される、 g_iData を増やし続ける関数。
void __cdecl IncrementThread1( void *p_p )
{
TRACE( "スレッド開始。\n" );
for( int iF1 = 0; iF1 < 10; ++iF1 )
{
// クリティカルセクションでロックを掛けます。
EnterCriticalSection( &g_stCriticalSection1 );
TRACE( "IncrementThread1() 1 と 2 の間。\n" );
// 0.1 秒待ちます。
// スレッド1がここで 0.1 秒待っている間に……
Sleep( 100 );
EnterCriticalSection( &g_stCriticalSection2 );
「で待っている間に」
// 別スレッドで呼び出される、 g_iData を増やし続ける関数。
void __cdecl IncrementThread2( void *p_p )
{
TRACE( "スレッド開始。\n" );
for( int iF1 = 0; iF1 < 10; ++iF1 )
{
EnterCriticalSection( &g_stCriticalSection2 );
TRACE( "IncrementThread2() 2 と 1 の間。\n" );
// 0.1 秒待ちます。
// スレッド2もここで待っていると……
Sleep( 100 );
// クリティカルセクションでロックを掛けます。
EnterCriticalSection( &g_stCriticalSection1 );
「にスレッド2が来ちゃうとデッドロックになります。で、重要なのは」
Sleep( 100 );
「の箇所。これがなくて、処理がとても短い場合、つまり」
EnterCriticalSection( &g_stCriticalSection1 );
「から」
LeaveCriticalSection( &g_stCriticalSection1 );
「までの時間が非常に短い場合」
・スレッド1が同期オブジェクト1にロックを掛けて、
同期オブジェクト2の待機を開始する前に、
スレッド2が同期オブジェクト2のロックを掛ける。
「この条件の〈同期オブジェクト2の待機を開始する前〉に〈スレッド2が
同期オブジェクト2のロックを掛ける〉のが非常に難しくなります」
『そか、 Sleep() で待たないから、同期オブジェクト1にロックを掛けて
とっとと処理しちゃって同期オブジェクト2の待機もとっととしちゃう、と
〈その間に〉ってほとんどないんだね』
「そういうこと。だから、こういう場合には〈確率的に〉デッドロックが発
生することになります」
『運が悪ければ、ってことね』
「ただし! 基本的にこれはあってはいけません」
『んー、確かに使ってる人から見れば〈運が悪かった〉じゃ済まないよね』
「そういうこと。普通にプログラムを組んでいればデッドロックが発生する
確率は 0% 。これが 0.00000001% になっただけでもダメ」
『……そこまで厳しくする必要あるの?』
「それは処理や使い方によるもの。 0.00000001% の確率でも、その処理を
1秒間に1万回行ったら? それを24時間365日ずっと走らせたら?」
『うわ、絶対に発生するねそれ……』
「もちろんこういう使い方をしちゃいけない、って言うこともできるけど、
それよりは」
『ちゃんとデッドロックは潰した方がいい、ってことね』
「そういうこと」
『んー、質問!』
「はい火美ちゃん」
『デッドロックは〈複数の同期オブジェクトを使う〉と発生しちゃうんだよ
ね。なら一個だけにすればいいんじゃない?』
「そうできたら苦労しないんだけど……」
『できないの?』
「同期オブジェクトは、基本的に〈同期を取りたい対象につきひとつ〉必要
です。たとえば、前回の例での g_iData のような変数が3つあったら」
『同期オブジェクトが3つ必要……』
「ということになります。だから、同期オブジェクトを複数持つことは避け
られません」
『……じゃあさ、その3つの変数をひとつの同期オブジェクトでロック掛け
るってゆーのは? たとえば……』
// 共有する変数。
int g_iData1 = 0;
int g_iData2 = 0;
int g_iData3 = 0;
『って変数がある時に』
void __cdecl IncrementThreadAll( void *p_p )
{
TRACE( "スレッド開始。\n" );
for( int iF1 = 0; iF1 < 10; ++iF1 )
{
// クリティカルセクションでロックを掛けます。
EnterCriticalSection( &g_stCriticalSection );
// g_iData1 への処理。
// g_iData2 への処理。
// g_iData3 への処理。
// クリティカルセクションのロックを解除します。
LeaveCriticalSection( &g_stCriticalSection );
}
TRACE( "スレッド終了。\n" );
}
『みたいに、ひとつの排他処理の中で全部の処理しちゃうのはどうかな。
g_iData1 を使うときにロック掛けて、その間は g_iData2 や g_iData3 に
もアクセスできなくする、みたいに』
「それなら同期オブジェクトはひとつだからデッドロックは起きないけど、
処理にすごく時間が掛かるようになるよ」
『え?』
「 g_iData1 に対する処理をしている間、他のスレッドが g_iData2 や
g_iData3 にアクセスしたら、そのスレッドは待機状態になっちゃうわけだ
から」
『 g_iData1 の処理が終わらない限り他のスレッドは止まったまま……それ
ってマルチスレッドの意味ないね』
「そういうこと。実際、同期の取り方は実行速度にとても影響します。前回
の例で、 EnterCriticalSection() と LeaveCriticalSection() の間の処理
がすごく時間の掛かる処理だったら」
『他のスレッドがその間待たなきゃいけない……』
「もし、そのすごく時間が掛かる処理が g_iData にまったく関係がなかっ
たら」
『げ、それって無駄!』
「そういうこと。だから、同期を取る、っていうのは必要最小限にする必要
があるんです」
『じゃ、これはダメなんだね……』
「ただ、こういうふうに処理しなきゃいけない場合もあるんです」
『え?』
「たとえば、 g_iData1 と g_iData2 の値を元に計算をして g_iData3 に結
果を入れる場合。この場合、 g_iData1 と g_iData2 のデータを〈同時に〉
取得する必要があります」
『同時?』
「 g_iData1 を取得して、そのあと g_iData2 を取得する前に、他のスレッ
ドが何か計算をして g_iData1 と g_iData2 を変えたりしたら、 g_iData1
と g_iData2 を使って計算する意味がないでしょ」
『んー、違う計算結果だから、ってこと?』
「そういうこと。 g_iData1 と g_iData2 が計算結果としてペアなら、この
ふたつの整合性が取れてないと意味ないし、これに加えて g_iData3 も同じ
ような意味だったら……」
『…… g_iData1 の取得から g_iData3 の計算が終わるまでずっとロックを
掛けてなきゃいけない……』
「そういうこと」
『……なんか、すっごくめんどいね』
「実は面倒なんです。で、これはスレッドとか同期オブジェクトに限らな
い、テーマの大きい問題なので、ここでは深くは見ません」
『それってつまり、取り上げるとそれだけで1章とか使っちゃうレベルって
こと?』
「そういうこと。それに、作り方によってはこういうふうに複雑にしない方
法はいくらでもあるから」
『たとえば?』
「今の例だと、先に g_iData1 と g_iData2 、 g_iData3 のデータを取って
きて、それから処理する」
『あ……』
「データを取ってくるところだけロックを掛けて、取ってきたらそれで計
算、ってすればいいわけ」
『なんだ、そんないい方法があるんじゃない』
「ただ、それは変数のコピーが可能な場合。変数がクラスでコピーが難し
かったり、データの量が大きかったりすると」
『無理だね……』
「でも、どういう状況でも〈使わない方法〉はあるものだから、とりあえず
はそういう方向で見てみて」
/*
Preview Next Story!
*/
『うわ、今回すっごくあいまい』
「ほんのさわりだからね。本質的にはすごく面倒な問題」
『うー、あんま扱いたくないね』
「そうもいかないんだけど、それはまた別のお話」
『ってことは次回は違う話?』
「次回は実用的な話」
『おお』
「というわけで次回」
< Version 14.23 メッセージの復習 >
『につづく!』
「そのまえにまずは復習」
『げげ』