「MFCの基礎 - マルチスレッド」の版間の差分

提供: MochiuWiki : SUSE, EC, PCB

 
(同じ利用者による、間の12版が非表示)
1行目: 1行目:
== 概要 ==
== 概要 ==
MFCアプリケーションにおいて、マルチスレッドを使用する方法について記述する。<br><br>
マルチスレッドを使用することにより、以下に示すようなメリットが得られる。
* UIの応答性向上
*: 時間の掛かる処理でもユーザインターフェースがフリーズしない。
* 並行処理の実現
*: 複数の処理を同時に実行し、処理時間を短縮できる。
* バックグラウンド処理
*: ファイルI/O、ネットワーク通信、データベースアクセス等を非同期で実行できる。
<br>
以下に示すような処理において、マルチスレッドの使用が効果的である。<br>
* 大量のファイルの読み込み・書き込み処理
* XMLやJSON等のデータのパース処理
* ネットワーク通信 (ダウンロード、アップロード)
* データベースへの大量データの挿入・更新
* 画像処理や動画処理等の計算量の多い処理
* 定期的な監視処理やポーリング処理
<br>
MFCでマルチスレッドを実装する場合の特徴は以下の通りである。<br>
* <code>AfxBeginThread</code> メソッドによりスレッドを作成することができる。
* <code>CWinThread</code> クラスでスレッドを管理する。
* UIコントロールは <u>スレッドセーフではない</u> ため、ワーカースレッドから直接操作できない。
* メインスレッドとの通信には <u>ウインドウメッセージ</u> を使用する。
* 共有リソースへのアクセスには <u>排他制御</u> が必要となる。
<br><br>
 
== スレッドの種類 ==
MFCでは、以下に示す2種類のスレッドを作成することができる。<br>
* ワーカースレッド
*: バックグラウンド処理を行うスレッド
*: メッセージループを持たず、時間の掛かる処理 (ファイルI/O、計算処理等) に使用する。
*: <br>
* UIスレッド
*: 独自のメッセージループを持つスレッド
*: ウインドウやダイアログを扱う場合に使用する。
*: 複数のウインドウを独立して動作させる場合等に使用する。
<br><br>


== 作成手順 ==
== 作成手順 : ワーカースレッド ==
まず、クラスにワーカースレッド関数(静的)とワーカースレッド本体の関数を追加する。<br>
# ワーカースレッド制御関数の定義
ワーカースレッド本体の関数では、静的でないメンバ変数およびメンバ関数が使用できる。<br>
#: まず、クラスに静的なワーカースレッド制御関数を定義する。
ワーカースレッド関数では、静的でないメンバ変数およびメンバ関数が使用できないと考えている人がいるが、<br>
#: <br>
下記のサンプルコードのようにすればクラスの関数はワーカースレッド関数にて行うことができる。<br><br>
#: ワーカースレッド制御関数は静的であるため、クラスのメンバ変数およびメンバ関数を直接使用することができない。
#: <u>ただし、<code>void*</code>型の引数を渡して特定のキャストを行うことにより、ワーカースレッド制御関数下でクラスのメンバを使用することができる。</u>
#: <br>
# ワーカースレッド本体の関数の定義
#: 次に、同じクラスに静的ではないワーカースレッド本体の関数を定義する。
#: ワーカースレッド本体の関数では、静的でないメンバ変数およびメンバ関数が使用できる。
#: <br>
# スレッドの開始と終了処理
#: <code>AfxBeginThread</code>関数でスレッドを作成・開始し、適切な終了処理を実装する。
<br><br>


== サンプルコード ==
== サンプルコード : ワーカースレッド ==
  <source lang="cpp">
==== ヘッダファイル ====
  // CFileView.h
  <syntaxhighlight lang="cpp">
  class CFileView
  // CFileView.hファイル
  class CFileView : public CDockablePane
  {
  {
    private:
private:
      CWinThread *m_pLoadXMLThread
    CWinThread       *m_pLoadXMLThread;    // スレッドオブジェクトへのポインタ
    CCriticalSection  m_csDataAccess;      // クリティカルセクション (排他制御用)
    volatile bool    m_bThreadRunning;    // スレッド実行中フラグ
   
   
     public:
     // ワーカースレッドで使用するメンバ変数
      static UINT LoadXMLThreadFunc(void* pParam); // XMLファイル読み込み時のスレッド制御関数
    CString          m_strXMLFilePath;    // XMLファイルのパス
      void       LoadXMLThreadFunc(); // 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()
  };
  };
</syntaxhighlight>
<br>
==== 実装ファイル ====
<syntaxhighlight lang="cpp">
// CFileView.cppファイル
   
   
  // 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()
  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読み込み処理スレッド関数を実行
     // XML読み込み処理スレッド関数を実行
     m_pLoadXMLThread = ::AfxBeginThread(LoadXMLThreadFunc, this, THREAD_PRIORITY_NORMAL,
    // thisポインタを渡すことで、静的関数内でメンバ変数にアクセス可能にする
                                        0, CREATE_SUSPENDED, NULL);
     m_pLoadXMLThread = ::AfxBeginThread(
     ASSERT(m_pLoadXMLThread);
      LoadXMLThreadFunc,           // スレッド関数
     if(m_pLoadXMLThread)
      this,                       // パラメータ (thisポインタ)
    {
      THREAD_PRIORITY_NORMAL,     // スレッドの優先度
       m_pLoadXMLThread->m_pMainWnd = this;
      0,                           // スタックサイズ (0: デフォルト)
       m_pLoadXMLThread->m_bAutoDelete = FALSE;
      CREATE_SUSPENDED,           // 作成フラグ
      NULL                         // セキュリティ属性
    );
     ASSERT(m_pLoadXMLThread != NULL);
     if (m_pLoadXMLThread) {
      // メインウィンドウを設定
       m_pLoadXMLThread->m_pMainWnd = this;
      // 自動削除を無効化(手動でdeleteする)
       m_pLoadXMLThread->m_bAutoDelete = FALSE;
   
   
       // スレッド処理の開始
       // スレッド処理の開始
       m_pLoadXMLThread->ResumeThread();
       m_pLoadXMLThread->ResumeThread();
       while(WaitForSingleObject(pTh->m_hThread, 100) == WAIT_TIMEOUT)
      {
      // ワーカースレッドが終了するまで待機
          // フリーズしないようにする
      // メッセージポンプを回してUIの応答性を維持
       while (::WaitForSingleObject(m_pLoadXMLThread->m_hThread, 100) == WAIT_TIMEOUT) {
           MSG msg;
           MSG msg;
           while(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
           while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
          {
             ::TranslateMessage(&msg);
             ::TranslateMessage(&msg);
             ::DispatchMessage(&msg);
             ::DispatchMessage(&msg);
           }
           }
       }
       }
       delete pTh;
      // スレッドオブジェクトを削除
       delete m_pLoadXMLThread;
      m_pLoadXMLThread = NULL;
      m_bThreadRunning = FALSE;
     }
     }
  }
  }
   
   
//----------------------------------------------------------------------------------
  // @breif 静的なワーカースレッド制御関数
// ワーカースレッド関数(静的)
  // @param この制御関数が定義されているクラスへのポインタ(void*型)
  // @brief   XMLファイル読み込み時のスレッド関数
  // @return スレッドの終了コード
  // @param[in]   pParam : 通常CFileViewクラスへのポインタが渡される
  //
  // @description  [説明]
  // @note 静的関数であるため、メンバ変数を直接使用することはできない
  //               XMLファイル読み込み処理を別スレッドで行う。
  //       しかし、引数として渡されたthisポインタをキャストすることにより、クラスのメンバ変数およびメンバ関数にアクセスすることが可能となる
//              ここでは処理本体を呼ぶだけである。
  // @return   成功時 0 を返します。
  //----------------------------------------------------------------------------------
  UINT CFileView::LoadXMLThreadFunc(void *pParam)
  UINT CFileView::LoadXMLThreadFunc(void *pParam)
  {
  {
    UINT nResult = 0;
    // void* から CWnd* にキャストした後、CFileView*にダイナミックキャスト
     CFileView *pFileView = dynamic_cast<CFileView*>(reinterpret_cast<CWnd*>(pParam));
     CFileView *pFileView = dynamic_cast<CFileView*>(reinterpret_cast<CWnd*>(pParam));
     if(pFileView)
     {
     if (pFileView) {
      pFileView->LoadXMLThreadFunc();
      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 THREAD_EXIT_CODE;
     return nResult;
  }
  }
   
   
  // ワーカースレッド本体の関数
  // ワーカースレッド本体の関数
  // スレッドの開始
  // 非静的メンバ関数であるため、メンバ変数を自由に使用できる
  void CFileView::LoadXMLThreadFunc()
  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;
}
</syntaxhighlight>
<br>
==== ウェイトダイアログを表示する例 ====
<syntaxhighlight lang="cpp">
// 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);
    }
  }
  }
  </source>
  </syntaxhighlight>
<br><br>
<br><br>


== 注意事項 ==
== 画面の更新 ==
UpdateData(TRUE)およびUpdateData(FALSE)等はスレッド内で呼ぶことができない。<br>
ワーカースレッド内において、<code>UpdateData</code> 関数等の画面を描画するような関数は実行することができない。<br>
AfxGetMainWnd()の戻り値は、CWinThread *m_pLoadXMLThread->m_pMainWndに渡したウィンドウである。<br>
これは、MFCのUIコントロールがスレッドセーフではないためである。<br>
設定しないとNULLが返り、また、ダイアログのCreate関数も呼ぶことができない。<br>
<br>
そこで、スレッドの処理と同期してUIを更新する場合(UpdateData(TRUE)をスレッドから同期して呼ぶ場合)は、<br>
<code>AfxGetMainWnd</code> 関数の戻り値は、<code>m_pLoadXMLThread->m_pMainWnd</code> に渡したウィンドウである。<br>
下記のようにスレッド内からメインウィンドウにメッセージを送る。<br>
設定しない場合は <code>NULL</code> となり、また、ダイアログの <code>Create</code> 関数も呼ぶことができない。<br>
<br>
そのため、ワーカースレッドの処理と同期して画面を更新する場合は、以下に示すようにワーカースレッド内からメインウィンドウにメッセージを送信する。<br>
メインウインドウは、メッセージを受信して任意の処理を行う。<br>
<br>
<br>
  <source lang="cpp">
==== ユーザ定義メッセージの定義 ====
  // スレッドからメイン処理にメッセージを送る & 受け取って処理
  <syntaxhighlight lang="cpp">
  // stdafx.hファイル
  //------------------------------------------------------------------------
  //------------------------------------------------------------------------
  // USER_MESSAGE
  // USER_MESSAGE
  //------------------------------------------------------------------------
  //------------------------------------------------------------------------
  // stdafx.h
  #define WM_USER_COMPLETE_LOAD_XML  (WM_USER + 1)  // XMLデータ読み込み完了
  #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)  // エラー発生
</syntaxhighlight>
<br>
==== SendMessageとPostMessageの違い ====
* SendMessage
*: 同期的にメッセージを送信する。
*: メッセージハンドラの処理が完了するまで制御が戻らない。
*: 戻り値を受け取ることができる。
*: <br>
* PostMessage
*: 非同期的にメッセージを送信する。
*: メッセージキューに追加後、即座に制御が戻る。
*: 戻り値を受け取ることができない。
<br>
<u>※注意</u><br>
<u>スレッドの終了を待機する <code>WaitForSingleObject</code> 関数を使用する場合、<code>SendMessage</code> でデッドロックになることに注意する。</u><br>
これは、<code>SendMessage</code> が同期的であるため、メインスレッドが <code>WaitForSingleObject</code> で待機中にワーカースレッドからの<code>SendMessage</code>を処理できないためである。<br>
<br>
これを解決するには、以下に示す方法がある。<br>
* 上記のサンプルコードのように、<code>WaitForSingleObject</code> 中にメッセージポンプを回す。
* <code>PostMessage</code> を使用して非同期でメッセージを送信する。
* スレッド終了時の処理をメッセージハンドラで行う。
<br><br>
 
== 排他制御 ==
複数のスレッドから同じデータにアクセスする場合、排他制御が必要である。<br>
MFCでは以下のクラスを使用して排他制御を行う。<br>
<br>
==== CCriticalSection ====
単一プロセス内での排他制御に使用。最も高速。<br>
<syntaxhighlight lang="cpp">
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;
    }
};
</syntaxhighlight>
<br>
==== CMutex ====
プロセス間の排他制御にも使用可能である。<br>
これは、名前付きで作成することができる。<br>
<br>
<syntaxhighlight lang="cpp">
CMutex m_mutex(FALSE, _T("MyAppMutex"));
void ThreadFunc()
{
    CSingleLock lock(&m_mutex, TRUE);
    // 排他制御が必要な処理
}
</syntaxhighlight>
<br>
==== CEvent ====
スレッド間でイベントを通知する。<br>
<br>
<syntaxhighlight lang="cpp">
CEvent m_eventComplete(FALSE, TRUE);  // 手動リセット、初期状態非シグナル
   
   
  // CFileView.cpp
  // ワーカースレッド内
  BEGIN_MESSAGE_MAP(CFileView, CDockablePane)
  void WorkerThread()
     // UserMessage
{
     ON_MESSAGE(WM_USER_COMPLETE_LOAD_XML, &CFileView::OnCompleteLoadXML)
     // 処理実行
END_MESSAGE_MAP()
     // ...
   
   
// CFileView.cpp
    // 処理完了を通知
// ワーカースレッド関数の本体にて
    m_eventComplete.SetEvent();
// XMLファイルの読み込みが完了したメッセージを送信
}
AfxGetMainWnd()->SendMessage(WM_USER_COMPLETE_LOAD_XML, (WPARAM)&firstIndex, (LPARAM)firstFilePath.GetString());
   
   
  // CFileView.h
  // メインスレッド
void MainThread()
{
    // 処理完了を待機
    ::WaitForSingleObject(m_eventComplete.m_hObject, INFINITE);
}
</syntaxhighlight>
<br><br>
 
== スレッドの終了 ==
==== 正常な終了方法 ====
* スレッド関数からreturnする (推奨)
* <code>AfxEndThread</code> を呼び出す
* <code>TerminateThread</code> は使用しない。 (強制終了でリソースリークの原因)
<br>
==== スレッドの中断処理 ====
<syntaxhighlight lang="cpp">
  class CFileView
  class CFileView
  {
  {
     // 追加
private:
     afx_msg LRESULT OnCompleteLoadXML(WPARAM wParam, LPARAM lParam );
     volatile bool m_bStopThread;  // スレッド停止フラグ
  }
  </source>
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;
          }
          // 処理実行
          // ...
      }
    }
};
</syntaxhighlight>
<br><br>
 
== 注意事項 ==
==== メモリリーク防止 ====
* <code>m_bAutoDelete = FALSE</code>の場合
*: 必ずスレッドオブジェクトを <code>delete</code> する。
* <code>m_bAutoDelete = TRUE</code>の場合
*: スレッド終了後に自動的に削除されるため、<code>delete</code> してはいけない。
<br>
==== UI操作の制限 ====
ワーカースレッドから直接UIを操作しない。<br>
必ずメッセージを使用する。<br>
<br>
==== 例外処理 ====
スレッド内で発生した例外はメインスレッドに伝播しないため、スレッド内で適切に処理する。<br>
<br>
==== デバッグ ====
マルチスレッドのデバッグは困難なため、<code>TRACE</code> マクロやログ出力を活用する。<br>
<br>
<br>
後は、SendMessage関数を受け取った側で処理を行えばよい。<br>
==== スレッド数の制限 ====
受け取り側では、UpdateData(TRUE)およびUpdateData(FALSE)等を呼ぶことができる。<br>
過度なスレッド作成はパフォーマンス低下の原因となる。<br>
ここでスレッドを待つWaitForSingleObject関数を使用した場合、デッドロックになるので気をつけること。<br>
必要に応じてスレッドプールを検討する。<br>
スレッドの終了時に何らかの処理を行うのであれば、今回のようにスレッド終了時に"メッセージをメインウィンドウへ送る"ことで解決できる。<br>
<br><br>
<br><br>
{{#seo:
|title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki
|keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,電気回路,電子回路,基板,プリント基板
|description={{PAGENAME}} - 電子回路とSUSE Linuxに関する情報 | This page is {{PAGENAME}} in our wiki about electronic circuits and SUSE Linux
|image=/resources/assets/MochiuLogo_Single_Blue.png
}}


__FORCETOC__
__FORCETOC__
[[カテゴリ:MFC]]
[[カテゴリ:MFC]]

2026年1月1日 (木) 04:00時点における最新版

概要

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

  • 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;     // スレッド実行中フラグ
 
    // ワーカースレッドで使用するメンバ変数
    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 マクロやログ出力を活用する。

スレッド数の制限

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