スレッドを作成する

さて、いよいよwindowsのマルチタスクの恩恵を受けるべく
スレッドを扱っていきたいと思います。

・マルチスレッドを使う意義

スレッドを扱う前に前置きです。実はスレッドというもの自体は、既に何気に使ってます。
それはメインスレッド(またはプライマリスレッド・親スレッド)といい、
WinMain() 関数ではじまり、数々の処理をこなし
WinMain() を抜けて終了する、という一連の動作を行っているものです。
スレッドというのはつまり、何かしらの一連の動作を行うもの、みたいな感じでしょうか。
また、アプリケーションプログラム自体をプロセスといいます。これはXPなどのOSを使っている人は
「プロセスの終了」というやつでおなじみだと思いますが。

ところが、このメインスレッドは何らかの処理を長期間行うために留まることが出来ません。
何故なら、メインスレッドはすぐにwindowsに処理を返さなくてはいけないからです。
(windowsのメッセージを取得しない=反応がない、つまりビジー状態になってしまいますからね)
そういった機能を実現することはシングルスレッドでも可能なのですが、複雑な処理になってしまいます。
そこで、副スレッドというwindowsの顔色を伺わずに処理を一貫して行えるものを使います。
そして、各スレッド同士が同時に別々の処理を平行することによって動作の効率化などを
実現させる技術をマルチスレッドなどといいます。
その時作られるスレッドは便宜上副スレッド・子スレッドなどと呼ばれます。

・CreateThread

今回は、メインスレッド(プロセス)からのインクリメントと副スレッドからのインクリメントを行います。
では、以下に処理の方法を書いていきます。

#include <windows.h>
#include <aygshell.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
//「メインスレッド」のインクリメント関数
void DoPaintMain (HWND) ;
//「インクリメント」スレッドファンクション
DWORD WINAPI fncThread (LPVOID) ;

//ウィンドウのハンドル
HWND g_hwnd ;
HDC g_hdc ;
// 「インクリメント」スレッドのハンドル
HANDLE hThread ;

PeekMessageのメインループで呼ばれる自作関数DoPaintMain 関数を、
また、副スレッドは関数として動作するので、その関数fncThread をそれぞれ宣言します。
作成されたスレッドのハンドルを格納する変数hThread も宣言します。

ループはPeekMessageのメインループを使います。

//==============================
//  ウィンドウプロシージャ
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
     {
     //デバイスコンテキスト
     HDC hdc ;
     PAINTSTRUCT paint ;
     static DWORD dThreadID;

この変数には、スレッドが作成された時スレッド識別子が格納されます。

          // ウィンドウ作成命令が出された
          case WM_CREATE :
               //デバイスコンテクスト取得
               g_hdc = GetDC (hwnd) ;
               SHDoneButton(hwnd, SHDB_SHOW );
               // スレッドを作成する
               hThread=CreateThread(NULL, 0, fncThread, 0, 0, &dThreadID);
               return 0 ;

デバイスコンテキストのハンドルを取得しておき、グローバルで使えるようにしておきます。
そして、CreateThread 関数でスレッドを作成します。
スレッドで使う関数fncThreadのポインタを渡し、hThread にはスレッドのハンドルが、
dThreadID にはスレッド識別子が格納されます。

//====================
//  「メインスレッド」のインクリメント関数
void DoPaintMain (HWND hWnd) 
{
     WCHAR wcStr[64];
     int static i;
     RECT Rect1={10,10,120,28};
     i++;
     wsprintf(wcStr, _T("MainLoop= %6d"),i);	
     DrawText(g_hdc, wcStr, -1, &Rect1, DT_LEFT);
}

この関数はメッセージループの隙間に呼ばれ、画面に描画していっています。
当然のことながら処理をすぐに返しています。

//==============================
//  「インクリメント」スレッドファンクション
DWORD WINAPI fncThread (LPVOID lpParameter) {

     WCHAR wcStr[64];
     int i=0;
     RECT Rect2={10,30,120,58};

     while (i<10000)
          {
          i++;
          wsprintf(wcStr, _T("Thread= %6d"),i);	
          DrawText(g_hdc, wcStr, -1, &Rect2, DT_LEFT);
     } //while

     return 0;
}

一方スレッド関数では処理を返す必要がないので、whileループ内だけで計算していっています。

では以上を踏まえたソースを実行してみましょう。

メインスレッドvs副スレッド

圧倒的じゃないか副スレッドの力は!ワハハ!
と思わず言いたくなってしまいました。
副スレッドは約2倍の速さでインクリメントされていきました。
メインスレッドは、windowsからのメッセージを監視しながらという
ながら戦術なので、キーを押したりタップされたりすると
負荷がかかってしまい、ますます遅くなっていってしまいました。
マルチスレッドの一貫した処理の有効性はこれで一応わかったということにしておきます。
(同期処理の問題などは後でしましょう)
実はこのプログラムには重要な欠陥が2つあるのですが・・・ァゥァ
それでも動作してしまうのはどうなんでしょうね・・・

・スレッドを扱うのは難しい

まず、マルチスレッドの場合別々のスレッドが同時に同じメモリ空間やファイルに
アクセスすることが考えられますが、そのような場合競合や衝突が起こり、最悪システムがハングアップします。
次は、副スレッドの終了に関することです。作られたスレッドはプログラム(プロセス)が終了すれば
プロセス上のスレッドは自動的に終了されるのですが、その時の副スレッドの動作を意識しなければなりません。
つまり、スレッドを終わらせたり止めた状態でプロセスを終了すべし、ということです。
最後に、取得したハンドルはCloseHandle などで解放しなければならなりません(メモリリークが起きます)。
なお、CloseHandle はスレッドが終了したのを確認してから行わなければいけません。

すいません勘違いしていました。プロセス終了時には、システムが自動的にハンドルをクローズするので
オープンされたスレッドオブジェクトはシステムによりすべて破棄される、ということのようです。
なので、CloseHandle で終了させる必要はないわけです。

「スレッドが終了し、CloseHandle 関数を使って、そのスレッドに関連する
 すべてのハンドルを閉じるまで、スレッドオブジェクトはシステム内に残ります。」
「スレッドのハンドルを閉じても、それに関連するスレッドは終了しません。
 スレッドオブジェクトを削除するには、最初にスレッドを終了し、
 次にそのスレッドのすべてのハンドルを閉じなければなりません。」MSDNの記述より

HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
・呼び出し側プロセスの仮想アドレス空間で実行するべき 1 個のスレッドを作成します
第一引数 lpThreadAttributes には、セキュリティ記述子を指定します (in)
第二引数 dwStackSize には、初期のスタックサイズを指定します (in)
第三引数 lpStartAddress には、スレッド機能の関数へのポインタを指定します (in)
第四引数 lpParameter には、スレッドの引数を指定します (in)
第五引数 dwCreationFlags には、作成オプションを指定します (in)
第六引数 lpThreadId には、スレッド識別子を指定します (out)

戻り値 成功なら新しいスレッドのハンドルが、失敗ならNULL が返ります

BOOL CloseHandle( HANDLE hObject );
・開いているオブジェクトハンドルを閉じます
第一引数 hObject には、開いているオブジェクトのハンドルを指定します

戻り値 成功ならTRUE が、失敗ならFALSE が返ります

ソースの表示

2005/6/26


戻る