S-JIS[2002-11-24/2006-04-22]

マルチスレッド

マルチスレッドとは、スレッドを複数同時に動かすことです。普通のアプリケーションは 基本的に1つのスレッドで動いています。
マルチスレッドと聞くと難しそうですが、MFCでは意外と簡単に出来ます。
ちなみに、「Windowsが擬似マルチスレッドだ〜」というような議論は、ここではしません。 よく分からないしぃ。

典型的なマルチスレッドの使い方としては、 画面表示やユーザーからの入力を行うインターフェーススレッドと、 計算をごりごり行うワーカースレッドの2つのスレッドに分けるものがあります。
例えば、ワーカースレッドで計算を行い、その計算の進み具合を インターフェーススレッドのプログレスバーで表示するというものです。


ここでは、「バージョン情報」のダイアログにプログレスバーを付け、 ワーカースレッドで値を1ずつ増加させる処理を行い、 プログレスバーにその状況を表示させるサンプルを示します。

まずは、ワーカースレッドを使わない形式のプログレスバーのプログラムを示します。 この構造については、プログレスバーの説明を参照して下さい。

test.cpp:
class CAboutDlg : public CDialog
{
public:
	CAboutDlg();

// ダイアログ データ
	//{{AFX_DATA(CAboutDlg)
	enum { IDD = IDD_ABOUTBOX };
	CProgressCtrl	m_Progress1;
	//}}AFX_DATA

  	// ClassWizard は仮想関数を生成しオーバーライドします。
	//{{AFX_VIRTUAL(CAboutDlg)
	protected:
	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV のサポート
	virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
	//}}AFX_VIRTUAL

// Implementation
protected:
	//{{AFX_MSG(CAboutDlg)
	virtual BOOL OnInitDialog();
	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()

private:
	int m_nValue;
	BOOL m_bDraw;
};

CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)
{
	//{{AFX_DATA_INIT(CAboutDlg)
	//}}AFX_DATA_INIT
	m_bDraw=FALSE;
}
〜
const int MAX=1000;

BOOL CAboutDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: この位置に初期化の補足処理を追加してください
	m_Progress1.SetRange(0,MAX);
	m_nValue=0;

	return TRUE;  // コントロールにフォーカスを設定しないとき、戻り値は TRUE となります
	              // 例外: OCX プロパティ ページの戻り値は FALSE となります
}

LRESULT CAboutDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
	// TODO: この位置に固有の処理を追加するか、または基本クラスを呼び出してください
	if(message==WM_PAINT) m_bDraw=TRUE;

	if(m_Progress1.m_hWnd && m_bDraw){
		if(m_nValue<MAX){
			m_nValue++;
		}
		m_Progress1.SetPos(m_nValue);
		UpdateData(FALSE);
	}

	return CDialog::WindowProc(message, wParam, lParam);
}

ここで行っている計算処理は、

  1. CAboutDlgクラスで定義したm_nValue
  2. OnInitDialogの中でで0に初期化し
  3. WindowProcの中でMAXまで1ずつ増加させる

というものです。


まずは、この計算処理を行う専用のクラスを作ります。
このクラスが、ワーカースレッドで実際の処理を行うことになります。

calc.h:
#ifndef	INCLUDE_CALC_H
#define	INCLUDE_CALC_H

class CCalc{
	int m_nValue,m_nMax;

public:
	void init(int max){	//初期化
		m_nValue=0;m_nMax=max;
	}

	void execute(){	//主処理
		while(m_nValue<m_nMax){
			m_nValue++;

			//負荷のかかる処理
			CString s;s.Format("%d",m_nValue);
		}
	}

	int getValue() const{	//値取得
		return m_nValue;
	}
};

#endif	//INCLUDE_CALC_H
init初期化 最大値の設定と、増加させる値(m_nValue)のクリア
execute主処理 m_nValueの増加処理。while文の中で単純に加算している。
負荷のかかる処理」は、 プログレスバーの動きを分かり易くする為の単なるウェイト
getValue値取得 m_nValueを返す

executeの中を見て下さい。単純にwhile文で 最大値になるまでm_nValueを増加させているだけです。 普通なら、while文が終わるまで他の処理は実行できません。
しかし、これをワーカースレッドとすることにより、 インターフェーススレッドからはgetValueを使って 計算中の値を取得することが出来るのです。


さっそく、作ったクラスをワーカースレッドとして呼び出すようにしましょう。
ワーカースレッドの開始には、AfxBeginThreadを使います。 ワーカースレッド本体となる関数を別途定義し、AfxBeginThreadに渡す必要があります。 AfxBeginThreadはスレッドを作成し、渡された関数を呼び出すのです。
この関数は引数を持っていて、ポインタを渡すことが出来ます。 このポインタを利用して、処理したいオブジェクトを受け渡せます。 プロセス内ではスレッドが異なってもメモリ空間は同一なので、ポインタ値はそのまま使えます。

test.cpp:
#include "calc.h"

class CAboutDlg : public CDialog
{
〜
private:
	CCalc m_Calc;
	BOOL m_bDraw;
};

static UINT AFX_CDECL ThreadProcCalc(LPVOID pParam)
{
	CCalc *cp=(CCalc*)pParam;
	cp->execute();
	return 0;
}

const int MAX=10000;

BOOL CAboutDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: この位置に初期化の補足処理を追加してください
	m_Progress1.SetRange(0,MAX);
	m_Calc.init(MAX);
	AfxBeginThread(ThreadProcCalc,&m_Calc);	//ワーカースレッドの開始:m_Calc.execute()を意味する

	return TRUE;  // コントロールにフォーカスを設定しないとき、戻り値は TRUE となります
	              // 例外: OCX プロパティ ページの戻り値は FALSE となります
}

マルチスレッド関連の関数が呼び出される順序は、

インターフェースワーカー
CCalc::init
AfxBeginThreadThreadProcCalc
OnInitDialogの
後続処理
CCalc::execute

です。
AfxBeginThreadが呼ばれた時点でワーカースレッドが作られ、ThreadProcCalcの実行が始まります。
また、AfxBeginThreadを呼び出した後のOnInitDialogの処理もそのまま続行されます。 この辺りがマルチスレッドのマルチスレッドたる所以で、慣れないと変な感じなんですが。


さて、ワーカースレッドを開始しただけではプログレスバーには反映されません。 プログレスバーの状況表示部分も変更します。

test.cpp:
LRESULT CAboutDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
	// TODO: この位置に固有の処理を追加するか、または基本クラスを呼び出してください
	if(message==WM_PAINT) m_bDraw=TRUE;

	if(m_Progress1.m_hWnd && m_bDraw){
		int value=m_Calc.getValue();
		m_Progress1.SetPos(value);
		UpdateData(FALSE);
	}

	return CDialog::WindowProc(message, wParam, lParam);
}

ワーカースレッドのexecuteで値を計算中なのですが、 getValueを使うことで計算中の値が取得でき、それをプログレスバーの表示に使っています。


ワーカースレッドは、AfxBeginThreadに渡した関数(今回の例ではThreadProcCalc)が 終了した時点で終了しますが、インターフェーススレッドは勝手に続いているため、 終わったかどうかを知る方法はありません。
CCalcクラスに計算が終了したかを訊ねる関数を追加したりするのが とりあえず無難な方法と思われます。

calc.h:
class CCalc{
	int m_nValue,m_nMax;
	BOOL m_bEnd;

public:
	void init(int max){	//初期化
		m_nValue=0;m_nMax=max;
		m_bEnd=FALSE;
	}
〜
	void setEnd(){
		m_bEnd=TRUE;
	}

	BOOL isEnd() const{	//終了したかどうか
		return m_bEnd;
	}
};
test.cpp:
static UINT AFX_CDECL ThreadProcCalc(LPVOID pParam)
{
	CCalc *cp=(CCalc*)pParam;
	cp->execute();
	cp->setEnd();
	return 0;
}

もしワーカースレッドの終了を待たずにインターフェーススレッドを終了する場合、 ただ単にインターフェーススレッドを終了すれば大丈夫っぽいです。
ただし、インターフェースとワーカーの双方で使っているオブジェクトを 片方で解放してしまうと、他方が使用しようとしてアクセス違反になることがあるので 注意が必要です。


排他制御メモへ行く / VC++ページへ戻る / 技術メモへ戻る
メールの送信先:ひしだま