MFCの基礎 - マルチスレッド

提供: MochiuWiki : SUSE, EC, PCB

2026年1月1日 (木) 03:51時点におけるWiki (トーク | 投稿記録)による版

概要

マルチスレッドを使用することにより、以下に示すようなメリットが得られる。

  • UIの応答性向上
    時間の掛かる処理でもユーザインターフェースがフリーズしない。
  • 並行処理の実現
    複数の処理を同時に実行し、処理時間を短縮できる。
  • バックグラウンド処理
    ファイルI/O、ネットワーク通信、データベースアクセス等を非同期で実行できる。


以下に示すような処理において、マルチスレッドの使用が効果的である。

  • 大量のファイルの読み込み・書き込み処理
  • XMLやJSON等のデータのパース処理
  • ネットワーク通信 (ダウンロード、アップロード)
  • データベースへの大量データの挿入・更新
  • 画像処理や動画処理等の計算量の多い処理
  • 定期的な監視処理やポーリング処理


MFCでマルチスレッドを実装する場合の特徴は以下の通りである。

  • AfxBeginThread メソッドによりスレッドを作成することができる。
  • CWinThread クラスでスレッドを管理する。
  • UIコントロールは スレッドセーフではない ため、ワーカースレッドから直接操作できない。
  • メインスレッドとの通信には ウインドウメッセージ を使用する。
  • 共有リソースへのアクセスには 排他制御 が必要となる。



スレッドの種類

MFCでは、以下に示す2種類のスレッドを作成することができる。

  • ワーカースレッド
    バックグラウンド処理を行うスレッド
    メッセージループを持たず、時間の掛かる処理 (ファイルI/O、計算処理等) に使用する。

  • UIスレッド
    独自のメッセージループを持つスレッド
    ウインドウやダイアログを扱う場合に使用する。
    複数のウインドウを独立して動作させる場合等に使用する。



作成手順 : ワーカースレッド

  1. ワーカースレッド制御関数の定義
    まず、クラスに静的なワーカースレッド制御関数を定義する。

    ワーカースレッド制御関数は静的であるため、クラスのメンバ変数およびメンバ関数を直接使用することができない。
    ただし、void*型の引数を渡して特定のキャストを行うことにより、ワーカースレッド制御関数下でクラスのメンバを使用することができる。

  2. ワーカースレッド本体の関数の定義
    次に、同じクラスに静的ではないワーカースレッド本体の関数を定義する。
    ワーカースレッド本体の関数では、静的でないメンバ変数およびメンバ関数が使用できる。

  3. スレッドの開始と終了処理
    AfxBeginThread関数でスレッドを作成・開始し、適切な終了処理を実装する。



サンプルコード : ワーカースレッド

ヘッダファイル

 // CFileView.hファイル
 
 class CFileView : public CDockablePane
 {
    private:
       CWinThread     *m_pLoadXMLThread;     // スレッドオブジェクトへのポインタ
       CCriticalSection m_csDataAccess;      // クリティカルセクション (排他制御用)
       volatile bool   m_bThreadRunning;     // スレッド実行中フラグ
 
    public:
       // 静的なワーカースレッド制御関数
       static UINT LoadXMLThreadFunc(void *pParam);
 
       // ワーカースレッド本体
       void LoadXMLThreadFunc();
 
       // メッセージハンドラ
       afx_msg LRESULT OnCompleteLoadXML(WPARAM wParam, LPARAM lParam);
 
       DECLARE_MESSAGE_MAP()
 };


実装ファイル

 // CFileView.cppファイル
 
 // メッセージマップ
 BEGIN_MESSAGE_MAP(CFileView, CDockablePane)
    ON_MESSAGE(WM_USER_COMPLETE_LOAD_XML, &CFileView::OnCompleteLoadXML)
 END_MESSAGE_MAP()
 
 // ボタンクリック時の処理
 void CFileView::OnButtonClick()
 {
    // 既にスレッドが実行中の場合は何もしない
    if (m_bThreadRunning) {
       AfxMessageBox(_T("処理が実行中です。"));
       return;
    }
 
    // スレッド実行中フラグを設定
    m_bThreadRunning = TRUE;
 
    // XML読み込み処理スレッド関数を実行
    // CREATE_SUSPENDED : 一時停止状態でスレッドを作成
    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;
    }
 }
 
 // 静的なワーカースレッド制御関数
 // 引数: この制御関数が定義されているクラスへのポインタ
 // 戻り値: スレッドの終了コード
 UINT CFileView::LoadXMLThreadFunc(void *pParam)
 {
    UINT nResult = 0;
    
    // void*からCWnd*にキャスト後、CFileView*にダイナミックキャスト
    CFileView *pFileView = dynamic_cast<CFileView*>(reinterpret_cast<CWnd*>(pParam));
    
    if (pFileView) {
       try {
          // 実処理はワーカースレッド本体の関数で行う
          pFileView->LoadXMLThreadFunc();
          nResult = 0; // 正常終了
       }
       catch (CException *e) {
          // 例外処理
          TCHAR szError[512];
          e->GetErrorMessage(szError, 512);
          TRACE(_T("スレッドエラー: %s\n"), szError);
          e->Delete();
          nResult = 1; // エラー終了
       }
       catch (...) {
          TRACE(_T("スレッドで不明なエラーが発生しました。\n"));
          nResult = 1; // エラー終了
       }
    }
 
    return nResult;
 }
 
 // ワーカースレッド本体の関数
 void CFileView::LoadXMLThreadFunc()
 {
    // ワーカースレッドで実行する処理
    // 例: XMLファイルの読み込み
 
    CString firstFilePath = _T("C:\\Data\\sample.xml");
    int firstIndex = 0;
 
    // 時間の掛かる処理を実行
    // ...略
 
    // 共有データへのアクセス時は排他制御を行う
    {
       CSingleLock lock(&m_csDataAccess, TRUE);
       // クリティカルセクション内でデータを操作
       // ...略
    } // lock変数のスコープを抜けると自動的にアンロック
 
    // XMLファイルの読み込みが完了したメッセージを送信
    // SendMessageは同期的に処理される (戻り値を受け取れる)
    AfxGetMainWnd()->SendMessage(
       WM_USER_COMPLETE_LOAD_XML,
       (WPARAM)&firstIndex,
       (LPARAM)firstFilePath.GetString()
    );
 }
 
 // メッセージハンドラ: XML読み込み完了時の処理
 LRESULT CFileView::OnCompleteLoadXML(WPARAM wParam, LPARAM lParam)
 {
    int *pIndex = reinterpret_cast<int*>(wParam);
    LPCTSTR pFilePath = reinterpret_cast<LPCTSTR>(lParam);
 
    // UI更新が可能 (メインスレッドで実行される)
    UpdateData(TRUE);
 
    // 処理完了のメッセージを表示
    CString msg;
    msg.Format(_T("XMLファイルの読み込みが完了しました。\nIndex: %d\nPath: %s"), *pIndex, pFilePath);
    AfxMessageBox(msg);
 
    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 マクロやログ出力を活用する。

スレッド数の制限

過度なスレッド作成はパフォーマンス低下の原因となる。
必要に応じてスレッドプールを検討する。