マルチスレッドプログラムの基本

単純なスレッドの起動とスレッドの終了待ち

新しくスレッドを作ることによって、ひとつのプロセス内に処理の流れを複数作ることが可能です。 C 言語のプログラムの場合、通常はメインスレッド (main thread) が main 関数を開始し、メインスレッドが main 関数を抜けて、 プログラムが終了します。複数の関数呼び出しがあっても、それぞれ一個ずつ順番に処理されます。 スレッド (thread) を作成すると、プログラムの複数の箇所が同時に実行されるようにすることが可能です。

Visual C++ の環境でスレッドを作成するときは、_beginthreadex を使います。_beginthreadex は内部で Win32 API の CreateThread を呼び出しそのハンドルを返します。またスレッドが終了してもそれを自動的に閉じたりしません。 (そのためハンドルをシグナルを待ったあとでも利用できる) また、開始アドレスとして受け取る関数は __stdcall の関数を受け取ります。

さっそくスレッドを作ってみましょう。

thread1.cpp として以下を保存し、ビルドして実行してください。

#include <windows.h>
#include <process.h>
#include <stdio.h>

unsigned int __stdcall ThreadFunc(PVOID pv)  {

     DWORD dwThread = GetCurrentThreadId();

     for ( int i=0; i<10; i++ ) {
          printf ( "[%u] %d\n", dwThread, i );
     }

     return 0;
}


int main( int argc, char* argv[] ) {

     DWORD dwThread;
     
     HANDLE hHandle = (HANDLE)_beginthreadex(
          NULL,          // security descripter 
          0,               // stack size
          ThreadFunc, //start address 
          NULL,          // arglist
          0,               // initial state. 0 - running
          (unsigned int*) &dwThread);
     
     if( !hHandle ) {
          printf( "_beginthreadex Failed.\n" );
          return 1;
     }

     WaitForSingleObject ( hHandle, INFINITE );

     CloseHandle ( hHandle );

     return 0;
}

makefile は以下。

TARGETNAME=thread1
OUTDIR=.\chk
LINK32=link.exe

ALL : "$(OUTDIR)\$(TARGETNAME).exe"


CPPFLAGS=\
	/nologo\
	/MT\
	/W3\
	/Fo"$(OUTDIR)\\"\
	/Fd"$(OUTDIR)\\"\
	/c\
	/Zi\
	/DWIN32\
	/DWIN32_WINNT=0x0600
		
LINK32_FLAGS=\
	/nologo\
	/subsystem:console\
	/pdb:"$(OUTDIR)\$(TARGETNAME).pdb"\
	/machine:I386\
	/out:"$(OUTDIR)\$(TARGETNAME).exe"\
	/DEBUG
	
LINK32_OBJS= \
	"$(OUTDIR)\$(TARGETNAME).obj"

"$(OUTDIR)\$(TARGETNAME).exe" : "$(OUTDIR)" $(LINK32_OBJS)
    $(LINK32) $(LINK32_FLAGS) $(LINK32_OBJS)

"$(OUTDIR)" :
    if not exist "$(OUTDIR)/$(NULL)" mkdir "$(OUTDIR)"

.c{$(OUTDIR)}.obj:
   $(CPP) $(CPPFLAGS) $<

.cpp{$(OUTDIR)}.obj:
   $(CPP) $(CPPFLAGS) $<

メインスレッドは WaitForSingleObject API を使って、新しく作成したスレッドが終了するのを待っています。 WaitForSingleObject はオブジェクトがシグナル状態になるまでそこでブロックします。スレッドは、そのスレッドの開始アドレスに 指定した関数 (スレッド関数) が終了すると終わります。スレッドが終了すると、そのスレッドを作成したときにカーネル内部に作成される、 スレッド (カーネル) オブジェクトがシグナル状態になります。ここではスレッドハンドルを渡して WaitForSingleObject を呼び出すことにより、 新しく作成したスレッドの終了を待っています。

複数のスレッドからの共有データへのアクセス

次はマルチスレッドプログラミングの難しいところを見てみましょう。

はじめに、 「複数のスレッドから同じデータにアクセスすると簡単にデータが壊れてしまう」 という実験をしてみましょう。

thread2.cpp として以下の内容を保存します。このコードではスレッドを二つ起動して、それらのスレッドから、 グローバル変数 g_nData を操作しています。また、メインスレッドは新しく起動したスレッドのハンドル二つを、 WaitForMultipleObjects 関数を使って待っています。

#include <windows.h>
#include <process.h>
#include <stdio.h>


LONG g_nData = 0;


unsigned int __stdcall ThreadFunc(PVOID pv)  {

     DWORD dwThread = GetCurrentThreadId();

     for ( int i=0; i<5000; i++ ) {
          g_nData++;
     }

     for ( int i=0; i<5000; i++ ) {
          g_nData--;
     }


     return 0;
}


int main( int argc, char* argv[] ) {

     DWORD dwThread[2];
     HANDLE hHandle[2];

     //
     // スレッド 1
     //
     
     hHandle[0] = (HANDLE)_beginthreadex(
          NULL, 0, ThreadFunc, NULL,
          CREATE_SUSPENDED, (unsigned int*) &dwThread[0]);
     
     if( !hHandle[0] ) {
          printf( "_beginthreadex Failed.\n" );
          return 1;
     }


     //
     // スレッド 2
     //
     
     hHandle[1] = (HANDLE)_beginthreadex(
          NULL, 0, ThreadFunc, NULL,
          CREATE_SUSPENDED, (unsigned int*) &dwThread[1]);
          
     if( !hHandle[1] ) {
          printf( "_beginthreadex Failed.\n" );
          return 1;
     }

     //
     // スレッドの開始
     //
     
     ResumeThread ( hHandle[0] );
     ResumeThread ( hHandle[1] );

     //
     // スレッドの終了を待つ
     //
     
     WaitForMultipleObjects ( 
          sizeof(hHandle)/sizeof(hHandle[0]),
          hHandle,
          TRUE,
          INFINITE );

     //
     // クリーンアップ
     //
     
     CloseHandle ( hHandle[0] );
     CloseHandle ( hHandle[1] );
     
     printf( "g_nData = %d\n", g_nData );

     return 0;
}

これをビルドし、実行すると結果は以下のようになります。

> thread2.exe
g_nData = 33

> thread2.exe
g_nData = 11

> thread2.exe
g_nData = -10

スレッド関数の中で、100 回 1 の足し算、100 回 1 の引き算をしているだけなので、結果は 0 を期待したいところですが、 実際の結果はご覧のように実行するたびに意味不明の数字となります。 データが破損しているのです (競合状態といいます)。

クリティカルセクションによるデータの保護

そこで、変数 g_nData をクリティカルセクションで保護することにより、競合状態を防ぎます。

thread3.cpp として以下を保存しビルドして実行してみてください。

#include <windows.h>
#include <process.h>
#include <stdio.h>


LONG g_nData = 0;
CRITICAL_SECTION g_cs;


unsigned int __stdcall ThreadFunc(PVOID pv)  {

     DWORD dwThread = GetCurrentThreadId();

     for ( int i=0; i<5000; i++ ) {
          EnterCriticalSection (&g_cs);
          g_nData++;
          LeaveCriticalSection(&g_cs);
     }

     for ( int i=0; i<5000; i++ ) {
          EnterCriticalSection(&g_cs);
          g_nData--;
          LeaveCriticalSection(&g_cs);
     }


     return 0;
}


int main( int argc, char* argv[] ) {

     DWORD dwThread[2];
     HANDLE hHandle[2];

     //
     // クリティカルセクションの初期化
     //
     
     InitializeCriticalSection (&g_cs);


     //
     // スレッド 1
     //
     
     hHandle[0] = (HANDLE)_beginthreadex(
          NULL, 0, ThreadFunc, NULL,
          CREATE_SUSPENDED, (unsigned int*) &dwThread[0]);
     
     if( !hHandle[0] ) {
          printf( "_beginthreadex Failed.\n" );
          return 1;
     }


     //
     // スレッド 2
     //
     
     hHandle[1] = (HANDLE)_beginthreadex(
          NULL, 0, ThreadFunc, NULL,
          CREATE_SUSPENDED, (unsigned int*) &dwThread[1]);
          
     if( !hHandle[1] ) {
          printf( "_beginthreadex Failed.\n" );
          return 1;
     }


     //
     // スレッドの開始
     //
     
     ResumeThread ( hHandle[0] );
     ResumeThread ( hHandle[1] );


     //
     // スレッドの終了を待つ
     //
     
     WaitForMultipleObjects ( 
          sizeof(hHandle)/sizeof(hHandle[0]),
          hHandle,
          TRUE,
          INFINITE );

     //
     // クリーンアップ
     //
     
     CloseHandle ( hHandle[0] );
     CloseHandle ( hHandle[1] );
     
     DeleteCriticalSection (&g_cs);


     printf( "g_nData = %d\n", g_nData );

     return 0;
}

> thread3.exe
g_nData = 0

> thread3.exe
g_nData = 0

> thread3.exe
g_nData = 0

>

想定したとおり、正しく計算されています。

クリティカルセクションの初期化は InitializeCriticalSection 関数で行い、使い終わったら DeleteCriticalSection で削除します。

データを守るために、EnterCriticalSection を呼び出してクリティカルセクションに入ってから共有データを操作する。 操作し終わったら LeaveCriticalSection でクリティカルセクションから抜ける、というようにします。 こうすることにより、クリティカルセクションに入られるスレッドが1つに限定されるために、データが破損しません。

InterlockedIncrement 関数

尚、LONG のような単純なデータを保護するだけならクリティカルセクションを使うまでも無く、便利な関数があります。 インクリメント、デクリメントそれぞれ、InterlockedIncrement 関数、InterlockedDecrement 関数です。 これらはインクリメント、デクリメントの処理をアトミックに行うため、競合状態になりません。

unsigned int __stdcall ThreadFunc(PVOID pv)  {

     DWORD dwThread = GetCurrentThreadId();

     for ( int i=0; i<5000; i++ ) {
          InterlockedIncrement( &g_nData );
     }

     for ( int i=0; i<5000; i++ ) {
          InterlockedDecrement( &g_nData );
     }


     return 0;
}

上記のスレッド関数に差し替えためすことで、競合状態にならないことが確認できます。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説