ワーカースレッドの作成方法

 「スレッド」、それは「処理単位」のことです。
 実行されるアプリケーションひとつひとつを「プロセス」と呼びます。プロセスは最初にスレッドをひとつ持ちます。ウィンドウをいくつも作り、ウィンドウプロシージャが複数あっても、スレッドが同じならメッセージはひとつひとつ処理されます。
 この問題はユーザーインターフェイスを著しく損ないます。例えばウィンドウのデバイスコンテキストに複雑な描画をするとします。描画に時間が掛かるため、ツールボックスに「停止」ボタンを作製しました。さて、いざ描画をシングルスレッド、つまりごく普通に処理した場合、最悪、描画中にはこのボタンを押すことすら不可能になります。
 この問題を解決する方法が、マルチスレッドです。新たにスレッドを作製し、そのスレッドの中でメモリデバイスコンテキストへの描画をさせ、最後にウィンドウデバイスコンテキストへとコピーしてスレッドを終了させる、という方法を取れば、先ほどのようにツールボックスのボタンが押せないなんてことはなくなります。
 では、この「マルチスレッド」という機能を実際に使ってみましょう。

 新たにスレッドを作製する場合、MFCでは「ワーカースレッド」「ユーザーインターフェイススレッド」の2種類作製することができます。ワーカースレッドは関数を作製し、その関数が終了したときにスレッドも削除されます。ユーザーインターフェイススレッドはクラスを作製するので、ワーカースレッドよりも色々なことができます。たいがいはウィンドウを作製し、ユーザーに対して様々な処理をします。
 今回はワーカースレッドの説明をします。ユーザーインターフェイススレッドについては「mtmdi」サンプルを参考にしてください。

スレッドの実行
 実行させる関数の作製はあとにして、始めに新たにスレッドを作製するための関数を説明します。その関数はAfxBeginThread()です。実際には次のようにコーディングします。


AfxBeginThread( ::ThreadFunc, &m_stThreadInfo );
	

 ここでは簡略化のため、省略できる引数はすべて省略しています。さて、この関数の第1引数::ThreadFuncは、新しく作製されたスレッド中で実行する関数です。この関数はのちに作製します。また、通常のC言語タイプの関数を使用するため、スコープ解決演算子を付けています。
 第2引数の&m_stThreadInfoは、ThreadFunc()関数の第1引数です。この引数はLPVOID型なので、ポインタなら何でもOKです。ここでは後で作製する構造体へのポインタを渡しています。

 このコードはスレッドを作製し、第1引数の関数を実行します。ここから、作製されたスレッドと元のスレッドが同時に実行されます。つまり、元スレッド(以下アプリケーションスレッド)ではAfxBeginThread()の後の行が何事もなかったかのように実行されていき、同時に作製されたスレッド(以下関数スレッド)でThreadFunc()関数が実行されているというわけです。

実行する関数の作製
 では、実際に関数を作製してみましょう。今回は手抜きして、AfxBeginThread()が実際に実行されるメンバ関数のすぐ上に作製してしまうことにします。見た目を気にする場合には、新たにヘッダーファイルとソースファイルを作製し、ヘッダーファイルに関数のプロトタイプ宣言を、ソースファイルに関数の定義を書き込んでください。
 AfxBeginThread()に渡す関数は、必ず次のような書式にしなければいけません。


UINT 関数名( LPVOID pParam );
	

 つまりここでは、


UINT ThreadFunc( LPVOID pParam )
{
	// まずはデータの変換ね。
	THREAD_INFO	*stThreadInfo = (THREAD_INFO *)pParam;

	// ここにしたいことを書く!!

	return 1;
}
	

 という形式になるということです。第1引数のpParamにはAfxBeginThread()の第2引数、つまりここでは&m_stThreadInfoが入っています。まず始めにこのポインタを型変換しておきましょう。この構造体の説明は後ほどします。
 さて、もしかしたら「はてな?」と思われた方もいらっしゃるでしょう。そう、戻り値があります。先ほど「アプリケーションスレッドではAfxBeginThread()の後の行が何事もなかったかのように実行されていき」と説明したのになぜに? と思われたでしょう。確かに、あの場では関数の戻り値を取得することができません。この戻り値を取得するには::GetExitCodeThread()関数を使用しなければいけません。これは取り立てて必要なことではないので、ここでは説明しません。ちなみに、デバッグモードでビルド・実行した場合には、トレースの結果としてスレッドの終了コードが表示されます。
 このreturnが実行された時点で関数は終了し、同時にスレッドは破棄されます。

 関数の中身の説明はのちほど(簡単に)説明するとして、それよりもまず、構造体の問題です。

構造体の作製
 構造体は、例えば次のようなものを作製します。


// スレッドの関数へと渡すための構造体。
typedef struct _THREAD_INFO {
	BOOL		*pbDraw;	//描画していいかどうか。
	HWND		hWnd;		//メッセージを送るためのハンドル。
	HWND		hLsCtWnd;	//リストコントロールのハンドル。
	CStringArray	*pcStrAry;	//アイテム情報の配列へのポインタ。
} THREAD_INFO;
	

 スレッドを呼び出しているクラスに、この型のメンバ変数を作製しておきます。さて、この構造体のメンバですが、ポインタとハンドルしかないのは偶然ではありません。ポインタとハンドルを使用するのは、アプリケーションスレッドのオブジェクトに、ワーカースレッドの関数がアクセスするためなのです。
 例えば、AfxBeginThread()の前に次のようにしておきます。


void CKABDlg::OnBtnTest() 
{
	// 外部関数へと情報を渡すため、構造体を初期化します。
	m_stThreadInfo.pbDraw = &m_bDraw;
	m_stThreadInfo.hWnd = GetSafeHwnd();
	m_stThreadInfo.hLsCtWnd = m_cFileLsCt.GetSafeHwnd();
	m_stThreadInfo.pcStrAry = &m_cStrAry;

	// スレッドを作製し、関数を実行します。
	AfxBeginThread( ::ThreadFunc, &m_stThreadInfo );
}
	

 こうすることでThreadFunc()関数は、アプリケーションスレッドが持つ4つのオブジェクトにアクセスできることになりました。ウィンドウは、直接::SendMessage()::PostMessage()を使うか、CWnd::Attach()を使ってクラスと結びつけて使用します。また、わざわざウィンドウハンドルを渡さずに、直接ポインタを渡してもうまくいくみたいです。
 また、変数へのポインタを持つことで、アプリケーションスレッドの情報を取得することもできます。例えば、スレッドが始まる前、m_bDrawにはTRUEが入っていたとします。ワーカースレッドの関数の中で、長い処理の間たびたびm_bDrawの中身をチェックさせます。ここで、アプリケーションのツールボックスの「停止」ボタンを押したときに、m_bDrawFALSEを入れれば、ワーカースレッドの関数はこのことを知ることができ、必要なら関数を終了させ途中でスレッドを閉じることができます。

 このようにして、ワーカースレッドの関数はアプリケーションスレッドのオブジェクトへとアクセスすることができます。ただし、注意しなければならないことがあります。それは同期です。

同期 ―― Synchronization
 このワーカースレッドをたて続けにふたつ作製したとします。ワーカースレッドが実行する関数の機能は、アプリケーションスレッドのCStringArray型のメンバm_cStrAryに文字列をどんどん追加していくというものだとしましょう。
 これは一見うまくいきそうに見えます。スレッド1が文字列を追加し、スレッド2が文字列を追加しと、どんどん文字列が追加されるイメージが沸いてきます。しかし、これは安易なイメージです。

 ウィンドウズはこのようにうまくやってはくれません。スレッド1が配列に文字列を追加しようとして、配列のサイズを取得し、文字列のサイズを取得し、配列を拡張し――と、ここでいきなり無理矢理スレッド2へと処理を移してしまうというようなことをウィンドウズは平気でします。スレッド2も同じようなことをし、偶然、文字列の追加まで終了してしまったとしたら、スレッド1へと移ったときにどのようなことになるか……。
 このイメージを実感する簡単な方法は、餅つきを思い浮かべることです。きねを突くスレッド1と餅をこねるスレッド2、このふたつのスレッドが何も考えずに行動すれば見る間にスレッド2の手は腫れ上がることでしょう。この問題を解決すること、それはふたつのスレッドがタイミングを取るために必要なこと、それは「同期」を取ること、ふたつのスレッドをシンクロさせることです。

 同期を取るにはいくつかの方法があります。例えば、スレッド中の関数が同時にふたつ以上実行されないようにしたり、m_cStrAryのような変数が同時にふたつ以上のスレッドからアクセスできないようにしたりすることができます。
 これらのことについては各自で調べてください(ってゆーか、わてはまだ知らんのです、同期は)。ま、考えてみればふたつのアプリケーションを同時に管理するようなものだから、難しいことは当然かも……。

(C)KAB-studio 1997 ALL RIGHTS RESERVED.