このチュートリアルでは、マルチスレッドプログラムについて説明する。 おもに、スレッド間におけるデータのやり取りの方法を研究する。
ソース | リソーススクリプト | 実行結果 |
|
前章ではプロセスには少なくとも1つのメインスレッドがあることを学んだ。 そのスレッドというのは、一連の実行コードのことで、プログラム中で自由にスレッドを作ることができる。 マルチタスクはシステムでいくつものプロセスを同時に実行させるものだが、 マルチスレッドはマルチタスクと考え方は同じで、 1つのプロセスでいくつものスレッドが同時に実行するものである。 実装においては、スレッドはマインプログラムと同時に実行されていく関数と考えることができる。 同じ関数をいくつも実行させることもできるし、 要求に応じた関数をいくつも実行させることができる。 マルチスレッドはWin32固有のもので、Win16には備わっていない。
スレッドは同じプロセス上で実行されるので、 グローバルに割り当てられたメモリ領域に確保したリソースはどのスレッドからでもアクセスできる。 しかしながら、作成されたスレッドには、それぞれ固有のスタックエリアを持っており、 当然、スレッド毎のローカル変数はプライベート(他のスレッドからアクセスできない)である。 スレッドは固有のレジスタセットも保持しており、 スレッドのスイッチ(切り替え)が起こったときにそれまで動作していたスレッドのレジスタの状態を記憶していったん中断し、 またその中断していたスレッドが起動されたときに、保持していたレジスタ状態を元に戻して処理を続行させる。 これは内部的にWindowsが行う処理である。
以下にスレッドの2つの分類の説明を記述する。
- ユーザインターフェイススレッド
このタイプのスレッドは自分のウィンドウをもち、Windowsからのメッセージを受け取る。そのユーザからメッセージを受けることによって何らかの処理を行うので、このような名前になっている。
このタイプのスレッドは Win16ミューテックスのルールを前提としており、ただ一つのユーザインタフェーススレッドしか16bitのuserカーネル、gdiカーネルを使用できない。(カーネルの排他制御)
Windows95 API関数群は、16bitコードで書かれており、Win16ミューテックスはWindows95に独特のものである。WindowsNTは32bitコードで記述されているため、当然Win16ミューテックスを使用していないので、NTで動作するユーザインターフェイススレッドはWindows95より軽快に動作する。- ワーカースレッド
このタイプのスレッドはウィンドウを生成しない。もちろんウィンドウメッセージを受け取ることはなく、主にバックグラウンドで与えられた処理を行う。それゆえ、このような名前となっている。Win32のマルチスレッドプログラムを行うときに以下のような戦略をとることをアドバイスする。 メインスレッドはユーザインターフェイス処理を行い、 他のスレッドにはバックグラウンドで泥臭い処理を行わせたほうがよい。 この方法だと、メインスレッドが課長で、他のスレッドは平社員のような関係で、 実際に仕事を行うのは担当員で、課長の仕事は部長から請けた仕事を平社員に割り当てることと、 その仕事の進捗を部長に報告することである。 もし、平社員に仕事を割り当てることなく自分で全てやってしまった場合、 部長への報告や部長から仕事を請ける余裕がなくなってしまう。
これと同じことがウィンドウでも発生し、 もし、メインスレッドで非常に時間のかかる処理を行っている場合、 その処理が終わるまでユーザの入力を処理できなくなってしまう。 なので、非常に時間のかかる処理を行うスレッドを新たに作成し、 メインスレッドがユーザの入力を待ち受けられるようにする必要がある。
ともかく、CreateThread関数をCALLすればスレッドが作成できる。プロトタイプは以下のとおりだ。
CreateThread proto lpThreadAttributes : DWORD,\ dwStackSize : DWORD,\ lpStartAddress : DWORD,\ lpParameter : DWORD,\ dwCreationFlags : DWORD,\ lpThreadId : DWORDCreateThread関数はCreateProcess関数に非常によく似ている。
- lpThreadAttributes
スレッドのセキュリティ属性を指定する。デフォルトのセキュリティ記述子を使用したければ、NULLにする。- dwStackSize
スレッドのスタックサイズを指定する。メインスレッドと同じサイズでよければ、NULLにする。- lpStartAddress-->
スレッド関数のポインタで、この関数がスレッドの仕事となる。この関数の仕様は、32ビット引数が1つで、32ビットの変数を返すようになっている。- lpParameter
スレッド関数に渡す引数- dwCreationFlags
スレッドの制御を行う。0 を指定すれば作成後すぐ実行され、CREATE_SUSPENDEDを指定すれば、停止した状態となる。- lpThreadId
この引数に新しく作成したスレッドのスレッドIDを格納する。CreateThread関数の呼び出しが成功すれば、新しく作成したスレッドのハンドルを返す。 失敗すればNULLが返る。 dwCreationFlagsにCREATE_SUSPENDEDをセットしていなければ、 作成と同時にスレッド関数が実行される。 もし、CREATE_SUSPENDEDが指定されていれば、 ResumeThread関数がCALLされるまで停止している。
スレッド関数が ret命令(少し違うがC言語で言う return)で制御を返した時、 スレッド関数がCALLする代わりに、Windowsが暗黙的にExitThread関数をCALLする。 もちろん、自分の作成したスレッド関数の中でExitThread関数をCALLすることもできるが、 意味が無い。
GetExitCodeThread関数をCALLすることにより、スレッドの終了コードを取得できる。 もし他のスレッドからあるスレッドを終了させたければ、 TerminateThread関数をCALLすればよいのだが、切羽詰った状況でないと使用してはならない。 というのは、TerminateThread関数を使用した場合、 対象のスレッドはクリーンアップコードの実行 (オープンしたファイルをクローズしたり、メモリを解放したりなど)も行わずに、 を即座に終了させるからである。
では、今度はスレッド間の共同作業について説明しよう。
主に以下の3つの項目からなる。
- グローバル変数の使用
- Windowsメッセージ
- イベント
まずは、グローバル変数を使用する方法の説明だが、 スレッド間でグローバル変数といったプロセスに確保されたリソースを共有できるので、 このグローバル変数を使用することにより、共同作業を行う。 しかしながら、この方法は「同期」という点に注意しなければならない。 例えば、10個のメンバを持つ構造体を2つのスレッドが同時に使用していたとしよう。 そして、片方のスレッドがその10個のメンバのうち5個目までデータの入れ替えをしていた時に、 Windowsが突然もう片方のスレッドに切り替えた場合、どうなるであろうか? その切り替わったスレッドは、矛盾したデータを使用することになる。 マルチスレッドプログラムにミスは許されず、 しかも、デバッグやメンテナンス作業が非常に複雑である。 得てしてこの手のバグというものは、ランダムに発生するため見つけ出すのが非常に難しい。
次にWindowsメッセージを使用する場合だが、 スレッドのタイプが全てユーザインターフェイススレッドであれば何の問題もなく、 双方向通信が可能である。 やらなければならないことは、自分の作成するスレッドで有効となるWindowsメッセージをカスタマイズするということだけだ。 基本値としてWM_USERを使用して、以下のようにWindowsメッセージをカスタマイズする。
WM_MYCUSTOMMSG equ WM_USER+100hWindowsはWM_USERより大きな値は使用しないことになっているので、 WM_USERを超える値を独自のWindowsメッセージの値として割り当てることができる。
ただしWindowsメッセージを使うこの方法では、 一つがユーザインターフェイススレッドで、もう一つがワーカースレッドだった場合、 それらのスレッド間で双方向に通信できない。 というのは、ワーカースレッドは自分のウィンドウを持っていないため、メッセージキューも当然無い。 したがってこの場合は、以下のような方法をとることになる。
ユーザインターフェイススレッド ------> グローバル変数 ----> ワーカースレッド ワーカースレッド ------> カスタムウィンドウメッセージ ----> ユーザインターフェイススレッド今回のサンプルでもこの方法を用いている。
最後のイベントを用いる方法だが、この場合はイベントオブジェクトをフラグの一種として考え、 イベントオブジェクトが「非シグナル状態」ならスレッドは停止していおりCPUタイムスライスは割り当てられない。 一方、もしイベントオブジェクトが「シグナル状態」だった場合、 Windowsはスレッドをたたき起こし、スレッドは割り当てられている仕事をはじめることになる。
|
example zipファイルをダウンロードして解凍すると、 thread1.exe という実行ファイルがあるので、起動してみよう。 そして、"Savage Calculation" というメニューアイテムをクリックする。 これは6億回 "add eax,eax" という実行コードを繰り返すもので、 その間、メインウィンドウに対してウィンドウを動かしたり、 メニューをクリックしたりといったあらゆる操作ができないことを確認せよ。 計算(6億回のループ)が終了すれば、メッセージボックスが表示され、 その後通常にメインウィンドウに対して操作が行える。
このような、ユーザに対して不適切なインターフェイスを避けるために、 計算ルーチンをワーカースレッドに処理してもらい、 メインスレッドはユーザからの入力を受け付けるようにした方がよい。 メインウィンドウの応答が多少遅くなるかもしれないが、反応はするので、 ユーザが何もできないという状況には陥らなくなる。
|
|
メインプログラムは普通のウィンドウでメニューアイテムがある。 ユーザが"Create Thread"メニューアイテムを選択したら、 プログラムは以下のような処理を行いスレッドを作成する。
.if ax==IDM_CREATE_THREAD mov eax,OFFSET ThreadProc invoke CreateThread,NULL,NULL,eax,NULL,0,ADDR ThreadID invoke CloseHandle,eax上記の処理は、ThreadProcという名前の関数を実行するスレッドを作成する、 というものだ。 この処理が成功すると、CreateThread関数はすぐに制御を返し、 ThreadProc関数が実行される。 ここで取得するスレッドハンドルは使用しないので、 すぐに閉じている。 そうしないと、ほんの少しではあるがメモリがもったいない。 ただ、スレッドハンドルを閉じることが スレッドを終了することではないということに気をつけなければならない。 単に、スレッドハンドルを今後使用することができない、ということになるだけだ。 (スレッドハンドルを使用してスレッドの制御をするのだが、今回の例ではそこまで詳しく解説しない)
ThreadProc PROC USES ecx Param:DWORD mov ecx,600000000 Loop1: add eax,eax dec ecx jz Get_out jmp Loop1 Get_out: invoke PostMessage,hwnd,WM_FINISH,NULL,NULL ret ThreadProc ENDPこれを見ればわかるとおり、 ThreadProc関数は尋常じゃないほどループし、 そのループがやっと終わった時に、 WM_FINISHメッセージをメインウィンドウに送信する。 WM_FINISHメッセージは以下のようにカスタマイズしてある。
WM_FINISH equ WM_USER+100hWM_USERに100を加える必要はないのだが、そうした方がより安全なのである。
ちなみに、WN_FINISHメッセージはこのウィンドウだけに有効なもので、 WM_FINISHメッセージがメインウィンドウに送られた時に、 計算が終了したと言うメッセージボックスが出力される。
"Create Thread" 関数をCALLした数だけ(ただし成功した場合)、 スレッドを作成できるので、複数のスレッドを作成することができる。 この例では、スレッド間のやりとりは一方通行で、 作成したスレッドがメインウィンドウに通知するだけでる。 もしメインウィンドウからワーカースレッドに対して、 メッセージを送信したければ、以下のようにやればよい。
- メニューに"Kill Thread"などの名前でメニューアイテムを作成する。
- コマンドを実行していいかどうかを判定するためのフラグ(コマンドフラグ)をグローバル領域に作成する。TRUEならスレッドをストップさせ、FALSEならそのままにする。
- ThreadProc関数内のループ中でそのコマンドフラグをチェックする。
ユーザが"Kill Thread"メニューアイテムをクリックすると、 メインプログラムはコマンドフラグをTRUEにする。 そして、そのコマンドフラグがTRUEかどうかを判定するThreadProc関数のループ内で TRUEだということを認識すると、スレッドを終了し、制御を返すことになる。