MFCでThreadを使ってみる

プログラムを作っていると、処理時間が長い作業に出会わすことがあると思います。GUIを使ったプログラムでは、処理時間が長い作業をすると作業が終わるまで動きません。待てばいいのですが、作業を中断できないのは困ります。そこで、使用するのがスレッド(Thread)です。スレッドを使うと、メインが動きながら重たい作業を処理することができます。もちろん、重たい処理を中断することもできます。ここではスレッドの基本的な使い方について説明します。

スレッドを一つ出す

まず、MFCのプロジェクトを作りましょう(ここではMFCApplication1)。ここではダイアローグベースでつくりました。あと、Static Textを追加して、m_labelという変数を追加しています(Static Textを右クリックして、変数の追加、m_labelで追加)。あと、クラスのpublicに

int thread_id, count;

を追加します。

そして、OKボタンを押した際のメソッドを次のように書きます(OKボタンをダブルクリックすれば自動でOKボタンを押した際のメソッドが生成されます)。

void CMFCApplication1Dlg::OnBnClickedOk()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	//CDialogEx::OnOK();
	thread_id = 0;
	count = 0;
	AfxBeginThread(MyThread, this);
}

動作は単純で、thread_idとcountを0にして先ほどのスレッドを一つつくります。スレッドをつくるにはAfxBeginThreadを使用します。MyThreadは関数で次に定義します。thisはCMFCApplication1Dlgオブジェクトのポインタです。

次に関数MyThreadを「MFCApplication1Dlg.cpp」に以下のような関数を定義します。

UINT MyThread(LPVOID p){
	CMFCApplication1Dlg* ptr = (CMFCApplication1Dlg*)p;
	CString ss;
	int id = ptr->thread_id;
	ptr->thread_id++;
	for (int i = 0; i < 10; i++){
		ss.Format(_T("Thread:%d, count:%d"), id, ptr->count);
		ptr->m_label.SetWindowTextW(ss);
		ptr->count++;
		Sleep(500);
	}
	return 0;
}

この関数がスレッドを実行した際の動作になります。countに1つずつ足して、Static Textに表示するだけです。thread_idはスレッド特有の番号として導入しています。pにAfxBeginThreadの引数であるthis(CMFCApplication1Dlgオブジェクトのポインタ)が入ります。pはCMFCApplication1Dlgオブジェクトのポインタなので、CMFCApplication1Dlgオブジェクトのポインタに変換してptrに代入しています。これで、ptrを使用すればStatic Textの表示を制御できるようになるわけです。

動作させると以下のようになります。

スレッド

OKを押すと、設置したStatic Textに

  • Thread:0, count:0
  • Thread:0, count:1
  • Thread:0, count:2
  • Thread:0, count:9

と表示されます。

スレッドの中断と再開

スレッドの中断と再開にはSuspendThreadとResumeThread命令を使います。これらはCWinThreadクラスのメソッドです。では、キャンセルボタンをおしたら中断し1秒後に再開するプログラムをつくります。

まず、

CWinThread* Thread;

をCMFCApplication1Dlgオブジェクトに追加します。そして、OKボタンを押した際の動作を

void CMFCApplication1Dlg::OnBnClickedOk()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	//CDialogEx::OnOK();
	
	thread_id = 0;
	count = 0;
	Thread = AfxBeginThread(MyThread, this);
}

と書き換えます。これで、スレッドの情報が入ったCWinThreadのポインタがわかります。このポインタを使って生成したスレッドを制御するわけです。

次に、ボタン1とボタン2を設置して、おした際の動作を

void CMFCApplication1Dlg::OnBnClickedButton1()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	DWORD    dwExitCode;
	if (GetExitCodeThread(Thread->m_hThread, &dwExitCode)){
		if (dwExitCode == STILL_ACTIVE){
			Thread->SuspendThread();
		}
	}
}


void CMFCApplication1Dlg::OnBnClickedButton2()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	DWORD    dwExitCode;
	if (GetExitCodeThread(Thread->m_hThread, &dwExitCode)){
		if (dwExitCode == STILL_ACTIVE){
			Thread->ResumeThread();
		}
	}
}

とします。GetExitCodeThreadによりスレッドの状態がわかります。GetExitCodeThreadの1つめの引数がスレッドのハンドルです。そして、dwExitCodeにスレッドが終わっているかの情報が入ります。ここで、ThreadはCWinThreadのポインタなので、Thread->m_hThreadによりスレッドのハンドルをわたしてやります。まだスレッドが動作しているなら、dwExitCodeがSTILL_ACTIVE(数値でいうと259)となりSuspendThreadやResumeThreadが実行されます。これで、ボタン1で中断し、ボタン2で再開するという流れができます。

スレッドの終了

スレッドを中断するのではなく終了させたい場合があると思います。その場合は、

bool flag_end;

をMFCApplication1Dlgクラスのプロパティに定義しておきます。そして、

void CMFCApplication1Dlg::OnBnClickedOk()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	//CDialogEx::OnOK();
	thread_id = 0;
	count = 0;
	flag_end = false;
	Thread = AfxBeginThread(MyThread, this);
}

として、事前にflag_endをfalseにしておき実行関数で

UINT MyThread(LPVOID p){
	CMFCApplication1Dlg* ptr = (CMFCApplication1Dlg*)p;
	CString ss;
	int id = ptr->thread_id;
	ptr->thread_id++;
	for (int i = 0; i < 10; i++){
		if (ptr->flag_end) break;
		ss.Format(_T("Thread:%d, count:%d"), id, ptr->count);
		ptr->m_label.SetWindowTextW(ss);
		ptr->count++;
		Sleep(500);
	}
	return 0;
}

とループのたびにチェックしてtrueになったら抜けるようにします。そして、ボタン1で

void CMFCApplication1Dlg::OnBnClickedButton1()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	flag_end = true;
}

とflag_endをtrueにすれば、OKです。これでボタン1を押すと、中断されます。

著者:安井 真人(やすい まさと)