この記事は、
Win32並行処理プログラミング入門30の続きです。前回はスレッドセーフとcoutについて書きました。今回はcoutを勧めない理由を詳しく書きます。
繰り返しになりますが、
coutの様な副作用を伴うグローバルオブジェクトを、並行処理する事はお勧めできません。文章だけだと分かり難いと思いますので、サンプルを挙げてその理由を解説します。
#include <iostream>
#include <windows.h>
#include <tchar.h>
#include <process.h>
using namespace std;
//インスタンスのポインタを保持するクラス
class InstanceAndParameter
{
private:
void* ins;
void* param;
public:
InstanceAndParameter( void* ins, void* param )
: ins( ins ), param( param ){}
void* getInstance() const { return ins; }
void* getParameter() const { return param; }
};
//並行的にコンソールへスレッドIDを表示するクラス
class ConcurrentPrint
{
private:
LPCRITICAL_SECTION _coutsection;
public:
ConcurrentPrint( LPCRITICAL_SECTION coutsection )
: _coutsection( coutsection ) {}
//スレッドから呼び出されるメソッド
static unsigned __stdcall CallPrintFunc( void* pvParam )
{
//パラメータのチェック
_ASSERTE( FALSE == IsBadReadPtr( pvParam,
sizeof( InstanceAndParameter ) )
&& "スレッドに渡されたパラメータが無効です" );
//インスタンスとパラメータを取り出してメソッドを実行
InstanceAndParameter* ip =
reinterpret_cast< InstanceAndParameter* >( pvParam );
ConcurrentPrint* obj =
reinterpret_cast< ConcurrentPrint* >( ip->getInstance() );
obj->PrintID();
return 0;
};
//スレッドIDを表示する
void PrintID()
{
EnterCriticalSection( _coutsection );
cout << "スレッドID【"
<< GetCurrentThreadId() << "】" << endl;
LeaveCriticalSection( _coutsection );
}
/* クリティカルセクションを勝手に削除してはならない
~ConcurrentPrint()
{
DeleteCriticalSection( _coutsection );
}
*/
};
//並行的にコンソールへ「ピヨ」を表示するクラス
class ConcurrentPiyo
{
private:
LPCRITICAL_SECTION _coutsection;
public:
ConcurrentPiyo( LPCRITICAL_SECTION coutsection )
: _coutsection( coutsection ) {}
//スレッドから呼び出されるメソッド
static unsigned __stdcall CallPiyoFunc( void* pvParam )
{
//パラメータのチェック
_ASSERTE( FALSE == IsBadReadPtr( pvParam,
sizeof( InstanceAndParameter ) )
&& "スレッドに渡されたパラメータが無効です" );
//インスタンスとパラメータを取り出してメソッドを実行
InstanceAndParameter* ip =
reinterpret_cast< InstanceAndParameter* >( pvParam );
ConcurrentPiyo* obj =
reinterpret_cast< ConcurrentPiyo* >( ip->getInstance() );
int count =
PtrToInt( reinterpret_cast< INT_PTR >( ip->getParameter() ) );
obj->Piyo( count );
return 0;
};
//スレッドIDを表示する
void Piyo( const int count )
{
EnterCriticalSection( _coutsection );
for ( int i = 0; i < count; i++ ) {
cout << "ピヨ";
}
cout << endl;
LeaveCriticalSection( _coutsection );
}
};
int _tmain( int, _TCHAR* )
{
//スレッド数の判定
const int piyoCount = 2;
const int printCount = 2;
const int threadCount = printCount + piyoCount;
if ( threadCount > MAXIMUM_WAIT_OBJECTS ) {
cout << "【エラー】" << endl;
cout << "指定するスレッド数が多すぎます。" << endl;
cout << "指定するスレッド数は"
<< MAXIMUM_WAIT_OBJECTS
<< "以下にして下さい。" << endl;
return -1;
}
//クリティカルセクションの準備
CRITICAL_SECTION coutsection;
InitializeCriticalSection( &coutsection );
//各種スレッドを実行
cout << "【各種メッセージを表示します】" << endl;
HANDLE hThreads[ threadCount ];
for ( int i = 0; i < printCount; i++ ) {
//スレッドのパラメータを用意する
ConcurrentPrint cprint( &coutsection );
InstanceAndParameter threadparam(
reinterpret_cast< void * >( &cprint ),
reinterpret_cast< void * >( ( INT_PTR ) 0 ) );
//スレッドを準備
hThreads[ i ] = ( HANDLE ) _beginthreadex (
__nullptr,
0U,
ConcurrentPrint::CallPrintFunc,
reinterpret_cast< void * >( &threadparam ),
CREATE_SUSPENDED, //直ぐには実行しない
__nullptr );
}
const int piyoNumber = 10;
for ( int i = printCount; i < threadCount; i++ ) {
//スレッドのパラメータを用意する
ConcurrentPiyo piyo( &coutsection );
InstanceAndParameter threadparam(
reinterpret_cast< void * >( &piyo ),
reinterpret_cast< void * >( ( INT_PTR ) piyoNumber ) );
//スレッドを準備
hThreads[ i ] = ( HANDLE ) _beginthreadex (
__nullptr,
0U,
ConcurrentPiyo::CallPiyoFunc,
reinterpret_cast< void * >( &threadparam ),
CREATE_SUSPENDED, //直ぐには実行しない
__nullptr );
}
for ( int i = 0; i < threadCount; i++) {
ResumeThread( hThreads[ i ] );
}
//タイムアウト時間をミリ単位で指定
//※指定した時間は正確ではありません
DWORD result = WaitForMultipleObjects(
threadCount, hThreads, TRUE, 2000 );
//WaitForSingleObject関数が終了した原因
cout << endl << endl;
cout << "【スレッドの状態を表示します】" << endl;
unsigned int max = ( WAIT_OBJECT_0 + threadCount - 1 );
if ( result == WAIT_FAILED ) {
//エラーを表示する
LPVOID title = _T( "エラー" );
LPVOID msg;
FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
__nullptr,
GetLastError(),
MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ),
reinterpret_cast< LPTSTR >( &msg ),
0,
__nullptr
);
MessageBox( __nullptr,
reinterpret_cast< LPCTSTR >( msg ) ,
reinterpret_cast< LPCTSTR >( title ),
MB_OK | MB_ICONERROR );
LocalFree( msg );
return -1;
}
if ( result == WAIT_TIMEOUT ) {
cout << "タイムアウトしてしまいました。" << endl;
} else if ( ( result >= WAIT_OBJECT_0 ) & ( result < max ) ) {
cout << "全スレッドの処理が終わりました。" << endl;
}
cout << endl;
//クリティカルセクションを削除
DeleteCriticalSection( &coutsection );
//ハンドルを閉じる
for ( int i = 0; i < threadCount; i++ ) {
CloseHandle( hThreads[ i ] );
hThreads[ i ] = __nullptr; //念のためNULLに設定
}
cout << endl;
return 0;
}
少し長いサンプルですがやっている事は非常に単純です。コンソールにスレッドIDとピヨを並行的に表示しているだけです。しかし問題点がいくつもあります。
第1の問題は、
クラスの仕様が多くなる事です。このサンプルでは、「必ずcout用のクリティカルセクションを持たねばならない」という仕様、「クリティカルセクションを勝手に削除してはならない」という仕様、「適切にクリティカルセクション操作用関数を使用せねばならない」という仕様の計3つの仕様が必要になります。何故ならば、cout用のクリティカルセクションを持たねばコンソール上の文字列は滅茶苦茶になり、勝手にクリティカルセクションを削除するインスタンスが存在すると、並行処理中の他のインスタンスが正常に処理を継続できなくなるからです。
守るべき仕様が多いクラスは、再利用性が低くテストも行い難くなります。守るべき仕様が多いクラスを設計してはなりません。
第2の問題は、
利用する側も条件を守る必要がある事です。このサンプルでは、利用側のmain関数内でクリティカルセクションの初期化と削除が必要となります。200行程度の短いプログラムならばあまり問題になりませんが、実務レベルでは数十万行以上のプログラムを分割して複数のプログラマが担当します。
複数のプログラマが守るべき条件が多くなるにつれて、作業効率が下がりバグの発生率が高くなります。従って、クラスを使用する条件を設ける事を極力避けるべきです。
第3の問題は
変更がやり難い事です。
グローバルオブジェクトを共有すると、クラス間の結合度が高まり、仕様変更の影響範囲が広くなってしまいます。例えば、同期方式を変更する場合、ある前提の下にグローバルオブジェクトを使用しているクラスを全て見直さなければなりません。そうしないと、並行処理特有のバグが発生する可能性が十分にあります。実務では先を見据えてプログラミングせねばなりません。
第4の問題は、
そもそも並行処理する必然性がない事です。例えば、今回のサンプルで並行的にコンソール上に文字を表示する必然性があるのでしょうか?そんな事をせずに値を返すようにしておけば、後でその文字列を直列的にコンソールへ出力できます。そして、後で仕様が変更されファイルへ出力する事になっても直ぐに対応できます。
以上のように、グローバルオブジェクトを用いた並行処理は問題が沢山存在します。問題が多い方式を積極的に採用する愚を犯さないように、グローバルオブジェクトを極力避けましょう。続く...
テーマ : プログラミング
ジャンル : コンピュータ