MFCの基礎 - マルチスレッド
概要
マルチスレッドを使用することにより、以下に示すようなメリットが得られる。
- UIの応答性向上
- 時間の掛かる処理でもユーザインターフェースがフリーズしない。
- 並行処理の実現
- 複数の処理を同時に実行し、処理時間を短縮できる。
- バックグラウンド処理
- ファイルI/O、ネットワーク通信、データベースアクセス等を非同期で実行できる。
以下に示すような処理において、マルチスレッドの使用が効果的である。
- 大量のファイルの読み込み・書き込み処理
- XMLやJSON等のデータのパース処理
- ネットワーク通信 (ダウンロード、アップロード)
- データベースへの大量データの挿入・更新
- 画像処理や動画処理等の計算量の多い処理
- 定期的な監視処理やポーリング処理
MFCでマルチスレッドを実装する場合の特徴は以下の通りである。
AfxBeginThreadメソッドによりスレッドを作成することができる。CWinThreadクラスでスレッドを管理する。- UIコントロールは スレッドセーフではない ため、ワーカースレッドから直接操作できない。
- メインスレッドとの通信には ウインドウメッセージ を使用する。
- 共有リソースへのアクセスには 排他制御 が必要となる。
スレッドの種類
MFCでは、以下に示す2種類のスレッドを作成することができる。
- ワーカースレッド
- バックグラウンド処理を行うスレッド
- メッセージループを持たず、時間の掛かる処理 (ファイルI/O、計算処理等) に使用する。
- UIスレッド
- 独自のメッセージループを持つスレッド
- ウインドウやダイアログを扱う場合に使用する。
- 複数のウインドウを独立して動作させる場合等に使用する。
作成手順 : ワーカースレッド
- ワーカースレッド制御関数の定義
- まず、クラスに静的なワーカースレッド制御関数を定義する。
- ワーカースレッド制御関数は静的であるため、クラスのメンバ変数およびメンバ関数を直接使用することができない。
- ただし、
void*型の引数を渡して特定のキャストを行うことにより、ワーカースレッド制御関数下でクラスのメンバを使用することができる。
- ワーカースレッド本体の関数の定義
- 次に、同じクラスに静的ではないワーカースレッド本体の関数を定義する。
- ワーカースレッド本体の関数では、静的でないメンバ変数およびメンバ関数が使用できる。
- スレッドの開始と終了処理
AfxBeginThread関数でスレッドを作成・開始し、適切な終了処理を実装する。
サンプルコード : ワーカースレッド
ヘッダファイル
// CFileView.hファイル
class CFileView : public CDockablePane
{
private:
CWinThread *m_pLoadXMLThread; // スレッドオブジェクトへのポインタ
CCriticalSection m_csDataAccess; // クリティカルセクション (排他制御用)
volatile bool m_bThreadRunning; // スレッド実行中フラグ
// ワーカースレッドで使用するメンバ変数
CString m_strXMLFilePath; // XMLファイルのパス
int m_nLoadProgress; // 読み込み進捗(0-100)
CArray<CString> m_arrXMLData; // 読み込んだXMLデータ
bool m_bLoadSuccess; // 読み込み成功フラグ
public:
// 静的なワーカースレッド制御関数
static UINT LoadXMLThreadFunc(void *pParam);
// ワーカースレッド本体
void LoadXMLThreadFunc();
// メッセージハンドラ
afx_msg LRESULT OnCompleteLoadXML(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnProgressUpdate(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
};
実装ファイル
// CFileView.cppファイル
// メッセージマップ
BEGIN_MESSAGE_MAP(CFileView, CDockablePane)
ON_MESSAGE(WM_USER_COMPLETE_LOAD_XML, &CFileView::OnCompleteLoadXML)
ON_MESSAGE(WM_USER_PROGRESS_UPDATE, &CFileView::OnProgressUpdate)
END_MESSAGE_MAP()
// ボタン押下時
void CFileView::OnButtonClick()
{
// 既にスレッドが実行中の場合は何もしない
if (m_bThreadRunning) {
AfxMessageBox(_T("処理が実行中です。"));
return;
}
// メンバ変数の初期化
m_strXMLFilePath = _T("C:\\Data\\sample.xml");
m_nLoadProgress = 0;
m_bLoadSuccess = FALSE;
m_arrXMLData.RemoveAll();
// スレッド実行中フラグを設定
m_bThreadRunning = TRUE;
// XML読み込み処理スレッド関数を実行
// thisポインタを渡すことで、静的関数内でメンバ変数にアクセス可能にする
m_pLoadXMLThread = ::AfxBeginThread(
LoadXMLThreadFunc, // スレッド関数
this, // パラメータ (thisポインタ)
THREAD_PRIORITY_NORMAL, // スレッドの優先度
0, // スタックサイズ (0: デフォルト)
CREATE_SUSPENDED, // 作成フラグ
NULL // セキュリティ属性
);
ASSERT(m_pLoadXMLThread != NULL);
if (m_pLoadXMLThread) {
// メインウィンドウを設定
m_pLoadXMLThread->m_pMainWnd = this;
// 自動削除を無効化(手動でdeleteする)
m_pLoadXMLThread->m_bAutoDelete = FALSE;
// スレッド処理の開始
m_pLoadXMLThread->ResumeThread();
// ワーカースレッドが終了するまで待機
// メッセージポンプを回してUIの応答性を維持
while (::WaitForSingleObject(m_pLoadXMLThread->m_hThread, 100) == WAIT_TIMEOUT) {
MSG msg;
while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
// スレッドオブジェクトを削除
delete m_pLoadXMLThread;
m_pLoadXMLThread = NULL;
m_bThreadRunning = FALSE;
}
}
// @breif 静的なワーカースレッド制御関数
// @param この制御関数が定義されているクラスへのポインタ(void*型)
// @return スレッドの終了コード
//
// @note 静的関数であるため、メンバ変数を直接使用することはできない
// しかし、引数として渡されたthisポインタをキャストすることにより、クラスのメンバ変数およびメンバ関数にアクセスすることが可能となる
UINT CFileView::LoadXMLThreadFunc(void *pParam)
{
UINT nResult = 0;
// void* から CWnd* にキャストした後、CFileView*にダイナミックキャスト
CFileView *pFileView = dynamic_cast<CFileView*>(reinterpret_cast<CWnd*>(pParam));
if (pFileView) {
try {
// pFileViewポインタを通じてメンバ変数にアクセス可能
// 例: pFileView->m_strXMLFilePath
// pFileView->m_nLoadProgress
// pFileView->m_arrXMLData
// 実処理はワーカースレッド本体の関数で行う
pFileView->LoadXMLThreadFunc();
nResult = 0; // 正常終了
}
catch (CException *e) {
// 例外処理
TCHAR szError[512];
e->GetErrorMessage(szError, 512);
TRACE(_T("スレッドエラー: %s\n"), szError);
e->Delete();
// メンバ変数に失敗フラグを設定
pFileView->m_bLoadSuccess = FALSE;
nResult = 1; // エラー終了
}
catch (...) {
TRACE(_T("スレッドで不明なエラーが発生しました。\n"));
pFileView->m_bLoadSuccess = FALSE;
nResult = 1; // エラー終了
}
}
return nResult;
}
// ワーカースレッド本体の関数
// 非静的メンバ関数であるため、メンバ変数を自由に使用できる
void CFileView::LoadXMLThreadFunc()
{
// メンバ変数を使用してXMLファイルのパスを取得
CString strFilePath = m_strXMLFilePath;
TRACE(_T("XMLファイルの読み込み開始: %s\n"), strFilePath);
// 時間のかかる処理をシミュレート
for (int i = 0; i < 10; i++) {
// 中断要求の確認
if (!m_bThreadRunning) {
TRACE(_T("処理が中断されました。\n"));
return;
}
// 進捗状況をメンバ変数に設定
{
CSingleLock lock(&m_csDataAccess, TRUE);
m_nLoadProgress = (i + 1) * 10; // 10%, 20%, ..., 100%
}
// 進捗状況をメインウインドウに通知
AfxGetMainWnd()->PostMessage(
WM_USER_PROGRESS_UPDATE,
(WPARAM)m_nLoadProgress,
0
);
// 実際の処理(例: XMLノードの読み込み)
// ...略
// ダミーデータをメンバ変数の配列に追加
CString strData;
strData.Format(_T("XMLデータ_%d"), i + 1);
{
CSingleLock lock(&m_csDataAccess, TRUE);
m_arrXMLData.Add(strData);
}
::Sleep(500); // 処理のシミュレート
}
// 読み込み成功フラグを設定
{
CSingleLock lock(&m_csDataAccess, TRUE);
m_bLoadSuccess = TRUE;
m_nLoadProgress = 100;
}
TRACE(_T("XMLファイルの読み込み完了 - データ数: %d\n"), m_arrXMLData.GetCount());
// XMLファイルの読み込みが完了したメッセージを送信
// メンバ変数の値をメッセージパラメータとして渡す
AfxGetMainWnd()->SendMessage(
WM_USER_COMPLETE_LOAD_XML,
(WPARAM)m_arrXMLData.GetCount(), // 読み込んだデータ数
(LPARAM)m_strXMLFilePath.GetString() // ファイルパス
);
}
// メッセージハンドラ : 進捗状況更新
LRESULT CFileView::OnProgressUpdate(WPARAM wParam, LPARAM lParam)
{
int nProgress = (int)wParam;
// プログレスバーの更新等
CString strMsg;
strMsg.Format(_T("読み込み中... %d%%"), nProgress);
// 画面に表示
// ...
return 0;
}
// メッセージハンドラ : XML読み込み完了時の処理
LRESULT CFileView::OnCompleteLoadXML(WPARAM wParam, LPARAM lParam)
{
int nDataCount = (int)wParam;
LPCTSTR pFilePath = reinterpret_cast<LPCTSTR>(lParam);
// UI更新が可能 (メインスレッドで実行される)
UpdateData(TRUE);
// メンバ変数から読み込み結果を取得
if (m_bLoadSuccess) {
CString msg;
msg.Format(_T("XMLファイルの読み込みが完了しました。\n")
_T("ファイル: %s\n")
_T("データ数: %d\n")
_T("進捗: %d%%"),
pFilePath, nDataCount, m_nLoadProgress);
AfxMessageBox(msg);
// 読み込んだデータをリストボックス等に表示
for (int i = 0; i < m_arrXMLData.GetCount(); i++) {
// リストボックスに追加
// m_listBox.AddString(m_arrXMLData[i]);
}
}
else {
AfxMessageBox(_T("XMLファイルの読み込みに失敗しました。"));
}
UpdateData(FALSE);
return 0;
}
ウェイトダイアログを表示する例
// CFileView.cppファイル
void CFileView::OnButtonClickWithWaitDlg()
{
if (m_bThreadRunning) {
return;
}
m_bThreadRunning = TRUE;
m_pLoadXMLThread = ::AfxBeginThread(
LoadXMLThreadFunc, this,
THREAD_PRIORITY_NORMAL, 0,
CREATE_SUSPENDED, NULL
);
if (m_pLoadXMLThread) {
// ウェイトダイアログを表示
CRect rc;
GetClientRect(&rc);
HWND hNotifyWnd = ::CreateDialog(
AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDD_PROCESSING),
this->m_hWnd,
(DLGPROC)YCommonProc
);
::ShowWindow(hNotifyWnd, SW_SHOW);
::MoveWindow(
hNotifyWnd,
(rc.right - rc.left) / 2 - 100,
(rc.bottom - rc.top) / 2 - 25,
200, 50,
TRUE
);
m_pLoadXMLThread->m_pMainWnd = this;
m_pLoadXMLThread->m_bAutoDelete = FALSE;
m_pLoadXMLThread->ResumeThread();
// メッセージポンプを回しながらスレッドの終了を待つ
while (::WaitForSingleObject(m_pLoadXMLThread->m_hThread, 100) == WAIT_TIMEOUT) {
MSG msg;
while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
delete m_pLoadXMLThread;
m_pLoadXMLThread = NULL;
m_bThreadRunning = FALSE;
// ウェイトダイアログを閉じる
::DestroyWindow(hNotifyWnd);
}
}
画面の更新
ワーカースレッド内において、UpdateData 関数等の画面を描画するような関数は実行することができない。
これは、MFCのUIコントロールがスレッドセーフではないためである。
AfxGetMainWnd 関数の戻り値は、m_pLoadXMLThread->m_pMainWnd に渡したウィンドウである。
設定しない場合は NULL となり、また、ダイアログの Create 関数も呼ぶことができない。
そのため、ワーカースレッドの処理と同期して画面を更新する場合は、以下に示すようにワーカースレッド内からメインウィンドウにメッセージを送信する。
メインウインドウは、メッセージを受信して任意の処理を行う。
ユーザ定義メッセージの定義
// stdafx.hファイル
//------------------------------------------------------------------------
// USER_MESSAGE
//------------------------------------------------------------------------
#define WM_USER_COMPLETE_LOAD_XML (WM_USER + 1) // XMLデータ読み込み完了
#define WM_USER_PROGRESS_UPDATE (WM_USER + 2) // 進捗状況更新
#define WM_USER_ERROR_OCCURRED (WM_USER + 3) // エラー発生
SendMessageとPostMessageの違い
- SendMessage
- 同期的にメッセージを送信する。
- メッセージハンドラの処理が完了するまで制御が戻らない。
- 戻り値を受け取ることができる。
- PostMessage
- 非同期的にメッセージを送信する。
- メッセージキューに追加後、即座に制御が戻る。
- 戻り値を受け取ることができない。
※注意
スレッドの終了を待機する WaitForSingleObject 関数を使用する場合、SendMessage でデッドロックになることに注意する。
これは、SendMessage が同期的であるため、メインスレッドが WaitForSingleObject で待機中にワーカースレッドからのSendMessageを処理できないためである。
これを解決するには、以下に示す方法がある。
- 上記のサンプルコードのように、
WaitForSingleObject中にメッセージポンプを回す。 PostMessageを使用して非同期でメッセージを送信する。- スレッド終了時の処理をメッセージハンドラで行う。
排他制御
複数のスレッドから同じデータにアクセスする場合、排他制御が必要である。
MFCでは以下のクラスを使用して排他制御を行う。
CCriticalSection
単一プロセス内での排他制御に使用。最も高速。
class CMyClass
{
private:
CCriticalSection m_cs;
CString m_strData;
public:
void SetData(const CString &str)
{
CSingleLock lock(&m_cs, TRUE); // コンストラクタでロック
m_strData = str;
// スコープを抜けるとデストラクタで自動的にアンロック
}
CString GetData()
{
CSingleLock lock(&m_cs, TRUE);
return m_strData;
}
};
CMutex
プロセス間の排他制御にも使用可能である。
これは、名前付きで作成することができる。
CMutex m_mutex(FALSE, _T("MyAppMutex"));
void ThreadFunc()
{
CSingleLock lock(&m_mutex, TRUE);
// 排他制御が必要な処理
}
CEvent
スレッド間でイベントを通知する。
CEvent m_eventComplete(FALSE, TRUE); // 手動リセット、初期状態非シグナル
// ワーカースレッド内
void WorkerThread()
{
// 処理実行
// ...
// 処理完了を通知
m_eventComplete.SetEvent();
}
// メインスレッド
void MainThread()
{
// 処理完了を待機
::WaitForSingleObject(m_eventComplete.m_hObject, INFINITE);
}
スレッドの終了
正常な終了方法
- スレッド関数からreturnする (推奨)
AfxEndThreadを呼び出すTerminateThreadは使用しない。 (強制終了でリソースリークの原因)
スレッドの中断処理
class CFileView
{
private:
volatile bool m_bStopThread; // スレッド停止フラグ
public:
void StopThread()
{
m_bStopThread = TRUE;
if (m_pLoadXMLThread) {
// スレッドの終了を待つ (タイムアウト付き)
DWORD dwRet = ::WaitForSingleObject(
m_pLoadXMLThread->m_hThread,
5000 // 5秒でタイムアウト
);
if (dwRet == WAIT_TIMEOUT) {
TRACE(_T("警告: スレッドが時間内に終了しませんでした。\n"));
}
}
}
void LoadXMLThreadFunc()
{
m_bStopThread = FALSE;
for (int i = 0; i < 1000; i++) {
// 定期的に停止フラグをチェック
if (m_bStopThread) {
TRACE(_T("スレッドを中断しました。\n"));
return;
}
// 処理実行
// ...
}
}
};
注意事項
メモリリーク防止
m_bAutoDelete = FALSEの場合- 必ずスレッドオブジェクトを
deleteする。
- 必ずスレッドオブジェクトを
m_bAutoDelete = TRUEの場合- スレッド終了後に自動的に削除されるため、
deleteしてはいけない。
- スレッド終了後に自動的に削除されるため、
UI操作の制限
ワーカースレッドから直接UIを操作しない。
必ずメッセージを使用する。
例外処理
スレッド内で発生した例外はメインスレッドに伝播しないため、スレッド内で適切に処理する。
デバッグ
マルチスレッドのデバッグは困難なため、TRACE マクロやログ出力を活用する。
スレッド数の制限
過度なスレッド作成はパフォーマンス低下の原因となる。
必要に応じてスレッドプールを検討する。