はじめに
筆者は今後絶対に身につけるべきプログラマーのスキルを、並列プログラミングだと考えています。その背景については『インテル Parallel Studioを使って並列化プログラミングを試してみた』を参照してください。
今回は、以前紹介できなかった並列プログラミングのデバッグと、デバッグツールについて紹介します。
並列プログラミングのデバッグ
並列プログラミングを行う際には、従来の逐次プログラミングの発想では思いつかないエラーが発生します。例えば次のようなエラーが発生します。
- 実行するたびに計算結果が違う。
- 不定期に予想もつかないエラーが発生する。
- システムがフリーズする。
こういった並列プログラミング特有のエラーは直すのが非常に困難です。なぜならば、この種のエラーは再現が困難であり、思いもよらないコードが原因となっているからです。
この様な状況下でよく行われることは、シングルステップなどのデバッガの機能を使用することです。しかし、厄介なことにデバッガがプログラムに介入することにより、並列性があるプログラムは挙動が変わります。従って、バグを突き止めるのは困難な作業となります。かといって、開発者の勘でエラーの原因に目星をつけてデバッグするのは非常に非効率的で、並列プログラミングをしていると専用のデバッガがすぐに欲しくなります。
そこで筆者はParallel Inspector(パラレル・インスペクター)を試すことにしました。
Parallel Inspectorの概要
Parallel Inspectorは、Parallel Studioに付属しているデバッガで、メモリーエラーやスレッド化エラーを検出することにより、前項で述べた様な並列プログラミング時に発生するさまざまなバグを取り除く手助けをしてくれる心強いツールです。並列プログラミングで発生するエラーも普通のプログラムと同じで発生箇所が分かれば怖くありません。エラーの発生箇所さえ検出できれば、大半の問題をクリアーしたと言えるでしょう。
どんな製品も試さないと分かりません。筆者は簡単なプログラムを書いて試してみました。次項でその様子をお伝えします。
Parallel Inspectorのメモリエラー解析機能は従来の直列プログラミングでも有効に働きます。そのため、並列化を行う前にシングルスレッドでの動作を検証し、安全なコードかどうかを事前に確認することもできます。
並列プログラミングのサンプル
まずはこの記事のサンプルプログラム「ParallelSample」をダウンロードしてください。このプログラムは、生産者(Procedure)が商品を製造して在庫数を増やし、消費者(Consumer)が商品を購入して在庫数を減らすというシンプルなものです。
#include <stdlib.h> #include <time.h> #include <limits.h> #include <iostream> #include <windows.h> #include <winnt.h> #include <tchar.h> #include <StrSafe.h> #include <process.h> using namespace std; int stockCount = 10; //在庫数 //商品の生産を行う関数 DWORD WINAPI Producer( PVOID pvParam ) { //増加数を読み出す int point = PtrToUlong(pvParam); //処理を行うために在庫数を読み出す //※DBかファイルから値を読み出していると仮定してください int tmp = stockCount; //商品の生産処理を行います(処理時間はランダム) time_t seed; time( &seed ); srand( seed % UINT_MAX ); Sleep( ( rand() / 1000 ) * ( rand() / 1000 ) ); //在庫数を反映させます stockCount = tmp + point; return 0; } //商品の販売を行う関数 DWORD WINAPI Consumer( PVOID pvParam ) { //増加数を読み出す int point = PtrToUlong(pvParam); //処理を行うために在庫数を読み出す //※DBかファイルから値を読み出していると仮定してください int tmp = stockCount; //商品の売り上げ処理を行います(処理時間はランダム) time_t seed; time( &seed ); srand( seed % UINT_MAX ); Sleep( ( rand() / 1000 ) * ( rand() / 1000 ) ); //在庫数を反映させます stockCount = tmp - point; return 0; } typedef unsigned (__stdcall *PTHREAD_START) (void *); int _tmain(int argc, _TCHAR* argv[]) { //初期処理 _tsetlocale( LC_ALL, _T("") ); int strCount = 20; TCHAR* buffer = ( TCHAR* ) malloc( strCount * sizeof( TCHAR ) ); StringCchCopy ( buffer, strCount, _T( "これから並列処理の実験を開始します・・・" ) ); _tprintf( buffer ); _tprintf( _T( "\n" ) ); /*--------------------------------------------------------------------------- 商品を生産してから商品を販売します。 このサイクルを任意の回数繰り返しながら、在庫数の推移を確認します。 -----------------------------------------------------------------------------*/ int x = 10; //増加数 DWORD dwThreadID; HANDLE hThreads[2]; cout << "初期在庫数:" << stockCount << endl; cout << "在庫の増減数:" << x << endl; cout << "****************************************" << endl; for ( int i = 0; i < 10; i++) { //処理前の在庫数を表示 cout << "処理前の在庫数:" << stockCount << endl; //生産者スレッド開始 hThreads[0] = ( HANDLE ) _beginthreadex ( ( void * ) NULL, ( unsigned ) 0, ( PTHREAD_START ) Producer, ( void * ) (PVOID) (INT_PTR) x, ( unsigned ) 0, ( unsigned * ) &dwThreadID ); //消費者スレッド開始 hThreads[1] = ( HANDLE ) _beginthreadex ( ( void * ) NULL, ( unsigned ) 0, ( PTHREAD_START ) Consumer, ( void * ) (PVOID) (INT_PTR) x, ( unsigned ) 0, ( unsigned * ) &dwThreadID ); //生産者と消費者の処理が終わるのを待つ WaitForMultipleObjects(2, hThreads, TRUE, INFINITE); //処理後の在庫数を表示 cout << "処理後の在庫数:" << stockCount << endl; //念のためにスレッドを閉じる CloseHandle( hThreads[0] ); CloseHandle( hThreads[1] ); } cout << "****************************************" << endl; cout << "最終在庫数:" << stockCount << endl; //終了処理 strCount = 4; buffer = ( TCHAR* ) malloc( strCount * sizeof( TCHAR ) ); StringCchCopy ( buffer, strCount, _T( "実験終了" ) ); _tprintf( buffer ); _tprintf( _T( "\n" ) ); cout << "Enterキーを押して終了してください。"; getchar(); return 0; }
このサンプルプログラムはParallel Inspectorの機能を試すために、あらかじめありがちなバグを埋め込んでいます。何度か実行してみてください。すると、最終在庫数が毎回違うことが確認できます。本来、増減数が同じで生産者と消費者が同じ数なのですから、初期在庫数と最終在庫数が同数になるはずです。また、よく確認すればメッセージがおかしいことも分かります。Parallel Inspectorを使ってこのエラーの原因を探ります。