◆マルチスレッド - VC++ 2005 MFC 〜 ユーザーインターフェイススレッド その2

[ ホーム ] [ 上へ ]

<プリマリスレッド ダイアログ実装>

前頁でセカンダリスレッド・そのメインウィンドウの実装をしたので、次に プライマリスレッド側の実装をします。といってもこちらはすでにスケルトンができている上に、 コードを記述するのはCStopWatchDlgだけです。
StopWatchDlg.h、StopWatchDlg.cppに以下のように記述してください。

(StopWatchDlg.h)

// StopWatchDlg.h : ヘッダーファイル
//
#pragma once

//前方宣言--------------------------
class CSecThread;
//----------------------------------

// CStopWatchDlg ダイアログ
class CStopWatchDlg : public CDialog
{
// コンストラクション
public:
    CStopWatchDlg(CWnd* pParent = NULL); // 標準コンストラクタ

// ダイアログデータ
    enum { IDD = IDD_STOPWATCH_DIALOG };

protected:
    virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV サポート

// 実装
protected:
    HICON m_hIcon;

// 生成された、メッセージ割り当て関数
    virtual BOOL OnInitDialog();
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();

//メンバ変数//////////////////////////////////////////////////////////////////
protected:
    CSecThread         *m_pSecThread;     //セカンダリスレッド用ポインタ

//メンバ関数//////////////////////////////////////////////////////////////////
protected:
    void TerminateSecThread();            //セカンダリスレッド終了
    afx_msg LRESULT OnTerminateApp(WPARAM wParam,  //アプリケーション終了ハンドラ
                                   LPARAM lParam); //(セカンダリスレッドからの終了通知)
                                                  
    virtual void OnOK();                  //CDialog::OnOKオーバーライド
    virtual void OnCancel();              //CDialog::OnCancelオーバーライド

public:
    afx_msg void OnBnClickedBtnStart();   //Startボタンクリック
    afx_msg void OnBnClickedBtnStop();    //Stopボタンクリック
    afx_msg void OnBnClickedBtnClear();   //Clearボタンクリック

protected:
    DECLARE_MESSAGE_MAP()
};

(StopWatchDlg.cpp)

// StopWatchDlg.cpp : 実装ファイル
//
#include "stdafx.h"
#include "StopWatch.h"
#include "StopWatchDlg.h"
#include "SecThread.h"
#include "DspWnd.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// CStopWatchDlg ダイアログ
 
CStopWatchDlg::CStopWatchDlg(CWnd* pParent /*=NULL*/)
    : CDialog(CStopWatchDlg::IDD, pParent)
    , m_pSecThread(NULL)
{
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CStopWatchDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CStopWatchDlg, CDialog)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    //}}AFX_MSG_MAP
    ON_BN_CLICKED(IDC_BTN_START, &CStopWatchDlg::OnBnClickedBtnStart)
    ON_BN_CLICKED(IDC_BTN_STOP, &CStopWatchDlg::OnBnClickedBtnStop)
    ON_BN_CLICKED(IDC_BTN_CLEAR, &CStopWatchDlg::OnBnClickedBtnClear)

    ON_MESSAGE(WM_TERMINATEAPP, &CStopWatchDlg::OnTerminateApp)
END_MESSAGE_MAP()

// CStopWatchDlg メッセージハンドラ
BOOL CStopWatchDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // このダイアログのアイコンを設定します。アプリケーションのメインウィンドウが
    // ダイアログでない場合、Framework は、この設定を自動的に行います。
    SetIcon(m_hIcon, TRUE);     // 大きいアイコンの設定
    SetIcon(m_hIcon, FALSE);    // 小さいアイコンの設定

    //-------------------------------------------------------------------------------
    //セカンダリスレッド構築
    m_pSecThread = (CSecThread *)AfxBeginThread(RUNTIME_CLASS(CSecThread),
                                                THREAD_PRIORITY_NORMAL,
                                                0,
                                                CREATE_SUSPENDED,
                                                0);
    m_pSecThread->m_bAutoDelete = FALSE;
    m_pSecThread->ResumeThread();
    //-------------------------------------------------------------------------------
    return TRUE; // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

// ダイアログに最小化ボタンを追加する場合、アイコンを描画するための
// 下のコードが必要です。ドキュメント/ビューモデルを使うMFC アプリケーションの場合、
// これは、Framework によって自動的に設定されます。
void CStopWatchDlg::OnPaint()
{
// ・
// ・
// ・
// ・
}

// ユーザーが最小化したウィンドウをドラッグしているときに表示するカーソルを取得するために、
// システムがこの関数を呼び出します。
HCURSOR CStopWatchDlg::OnQueryDragIcon()
{
    return static_cast<HCURSOR>(m_hIcon);
}

//////////////////////////////////////////////////////////////////////////////////////
//セカンダリスレッド終了
void CStopWatchDlg::TerminateSecThread()
{
    ::WaitForSingleObject(m_pSecThread->m_hThread, INFINITE);
    delete m_pSecThread;
    m_pSecThread = NULL;
}

//アプリケーション終了メッセージハンドラ
//(セカンダリスレッドからメインウィンドウ(CDspWnd)破壊時にメッセージ送信)
LRESULT CStopWatchDlg::OnTerminateApp(WPARAM wParam, LPARAM lParam)
{
    TerminateSecThread();

    CDialog::OnOK();            //ダイアログ終了
    return 0;
}

//セカンダリスレッドのメインウィンドウを破壊要求
void CStopWatchDlg::OnOK()
{
    m_pSecThread->m_pMainWnd->PostMessageW(WM_CLOSE, 0, 0);
}

//セカンダリスレッドのメインウィンドウを破壊要求
void CStopWatchDlg::OnCancel()
{
    m_pSecThread->m_pMainWnd->PostMessageW(WM_CLOSE, 0, 0);
}

//ストップウォッチStart
void CStopWatchDlg::OnBnClickedBtnStart()
{
    CDspWnd *pDsp = (CDspWnd *)m_pSecThread->m_pMainWnd;
    pDsp->Start();
}

//ストップウォッチStop
void CStopWatchDlg::OnBnClickedBtnStop()
{
    CDspWnd *pDsp = (CDspWnd *)m_pSecThread->m_pMainWnd;
    pDsp->Stop();
}

//データクリア
void CStopWatchDlg::OnBnClickedBtnClear()
{
    CDspWnd *pDsp = (CDspWnd *)m_pSecThread->m_pMainWnd;
    pDsp->Clear();
}

以上ですべての実装はできたのでビルドして実行してください。

<プログラム解説>

マルチスレッドがテーマなのでストップウォッチに関する機能のことは書きません。 ユーザインターフェイススレッドでは、スレッドの起動・破棄だけでも結構ややこしいので、 同期に関することは盛り込んでないです。

(起動)

プログラムを実行するとCStopWatchAppの唯一のインスタンス theAppのInitInstanceが呼ばれてオーバーライドされたCStopWatchApp::InitInstance()で CStopWatchDlgダイアログが構築されてCStopWatchDlg::OnInitDialog がフレームワークから呼ばれます。ここまではプライマリスレッド内の話です。
ここで第一引数にCSecThread型のRuntimeクラス情報を渡して、 AfxBeginThreadヘルパー関数を呼び出すと CRuntimeClass::CreateObjectを使って動的にCSecThreadインスタンスが作られます。そして、 AfxBeginThreadはCWinThread::CreateThreadを呼びますが、第四引数にCREATE_SUSPENDを指定している のでスレッドを作りますが、保留状態になります。

CWinThread::m_bAutoDeleteはスレッドが終了するとき、つまり、CWinThread::Runの ループが抜けて制御関数_AfxThreadEntryが 終了するときに自動的にCWinThread(CSecThread)インスタンスを削除するかどうかのフラグです。 その仕組みは_AfxThreadEntryの最後に AfxEndThreadが呼ばれてスレッドを終了していますが、その最後のほうで
pThread->Delete();
が実行されています。pThreadはCWinThreadへのポインタでDelete()の内容が
if (m_bAutoDelete) { delete this};
となっています。ということでこのメンバがTRUEの時はスレッドの終了とともに CWinThreadインスタンス(今回の場合はCSecThread)も終了します。

この後、CWinThread::ResumeThread()が呼ばれて保留になっていたセカンダリスレッドが 再開されます。InitInstanceで先ほど実装した CDspWndを構築&表示してメッセージポンプを起動しています。一方プライマリスレッドも、 AfxBeginThreadを呼んでセカンダリスレッドであるCSecThreadへのポインタを取得したら、 OnInitDialogを終了してダイアログを表示します。

(終了)

起動の場合は割りと素直にできますが、問題は終了の場合です。 例えば、表示ウィンドウが表示された状態で、ダイアログが閉じられたらどうでしょう? セカンダリスレッドのウィンドウの終了できないし、スレッドの終了処理もないままに アプリケーションが終了していまいメモリリーク等重大な問題が発生します。
起動時はプライマリスレッド→セカンダリスレッドの順で作られているので、表示ウィンドウ (CDspWnd)、操作ウィンドウ(CStopWatchDlg)のどちらが閉じられようとしても、 基本的に、CDspWnd(セカンダリのメインウィンドウ)→CSecThread(セカンダリスレッド) →CStopWatchDlg(プライマリのメインウィンドウ)→CStopWatchApp(プライマリスレッド)の順に 終了しないといけません。今回はアプリケーションとしてプライマリとセカンダリの 各ウィンドウは一体となっていて、どちらか一方が終了したら意味がないのでこういうことを いうのですが、アプリによっては作業が終了したらセカンダリだけ終わらせるということもあります。 いずれにしても、セカンダリだけが残っている状況はまずいということです。 この辺のオブジェクトの生存確認は、

セカンダリ側は
CDspWnd::OnDestroy

プライマリ側は
CStopWatch::TerminateSecThread
CStopWatch::OnTerminateApp
CStopWatch::OnOK
CStopWatch::OnCancel
が担当しています。

最初にCDspWndが閉じられる場合です。WM_DESTROYによってOnDestroyが呼ばれます。 ここで、「CDspWndが閉じられるので、セカンダリスレッドの終了を待ってCStopWatchDlgも 終了してください」という意味で、プライマリのCStopWatchDlgにユーザ定義の WM_TERMINATEAPPメッセージを送ります。WM_TERMINATEAPPを受け取ったCStopWatchDlgは ハンドラのOnTerminateAppでTerminiteSecThreadを呼び出してセカンダリスレッドCSecThreadが 終了するのを待ちます(WaitForSingleObject)。
ここで、CDspWndを閉じようとしているだけなのに、なぜセカンダリスレッド(CSecThread)まで 終了するのがわかるのかという疑問を持たれるかもしれません。 それはセカンダリスレッド側のメインウィンドウ (CDspWnd)が閉じられると自動的にスレッドが終了するようにCSecThread::m_pMainWndにCDspWndが 設定したことを思い出すとCDspWndの終了=CSecThreadの終了が成立することがわかると思います。 セカンダリスレッドの終了を確認したら、CDialog::OnOKを呼んでプライマリスレッドのダイアログを 閉じてアプリケーションを終了します。

問題はプライマリスレッドのCStopWatchDlgを先に閉じようとした場合です。 CDialogの派生クラスの既定ではフレームワークからOnOKまたはOnCancelが呼ばれてその中で EndDialogがコールされ閉じられます。それでは、セカンダリスレッド側が残ってしまいまずいので、 この2つの関数をオーバーライドして既定の動作を変更して、先にセカンダリのCDspWndウィンドウを 閉じるように変更します。といっても簡単でCDspWndにWM_CLOSEメッセージをポストするだけです。 あとはCDspWndが先に閉じられる場合と同じことです。

実際は、スレッド間で共有データを使うことがほとんどだと思うのですが、 ユーザインターフェイススレッドの場合はかなり複雑なので、共有データをつかった同期については 一切触れてません。しかし、マルチスレッドで本当に大変なのはこの同期についてだと思うので プロセス間通信ワーカースレッド でもすこし書いているので確認してみてください。