・スレッド作成の実際(ワーカースレッド)- WindowsXP + VC++ 2005
では実際に次のようなプロジェクトを作って、マルチスレッドの効果を確かめてみます。
次のようなダイアログ形式のプロジェクトを作ってください。
メニューのファイル(F)→新規作成(N)→プロジェクト(P)でアプリケーションウィザードから
プロジェクトの種類(P) : Visual C++ テンプレート : MFCアプリケーション プロジェクト名 : MultiThread(任意) →OK →次へ アプリケーションの種類 : ダイアログベース(D) (他はデフォルト) →次へ メインフレームのスタイル : システムメニュー(S)のみチェック →次へ 高度な機能 : (デフォルト) →次へ →完了
次にダイアログリソースを作成します。 ツールバーの表示(V)→その他のウィンドウ(E)→リソースビュー(R)でリソースビューを表示して ダイアログを以下のように作成してください。
各コントロールを右クリックしてプロパティ(R)を実行して プロパティシートを表示後、以下のようにプロパティ・管理イベントを設定してください。
コントロールタイプ | プロパティ ・ イベント | 値 | |
1 | スタティックテキスト | IDプロパティ | IDC_STC_PRIMARY |
Client Edgeプロパティ | True | ||
2 | ボタン | IDプロパティ | IDC_BTN_PRIMARY |
BN_CLICKEDイベント | <追加> OnBnClickedBtnPrimary |
||
3 | スタティックテキスト | IDプロパティ | IDC_STC_SECONDARY |
Client Edgeプロパティ | True | ||
4 | ボタン | IDプロパティ | IDC_BTN_SECONDARY |
BN_CLICKEDイベント | <追加> OnBnClickedBtnSecondary |
このようなダイアログで、2のボタンを押すたびに1加えてスタティックテキストに表示し、
4のボタンを押すと、3000万回の乱数を生成してそれを加算して表示します。4のボタンは
重い処理でCoreSoro-1.6GHzで約6,7秒かかりました。これよりCPU処理能力が劣る場合はさらに時間が
かかるので乱数の発生回数を調整してください。計算中の間は3のスタティックテキストに
「計算中・・・・」が表示されるようにします。
4のボタンを押した後、計算中の間に2のボタンをおして結果が表示されることを確認します。
<シングルスレッドの場合>
もし、上記のことをスレッドを作らずにやるとどうなるかを確認してみます。
// MultiThreadDlg.h : ヘッダーファイル // // CMultiThreadDlg ダイアログ class CMultiThreadDlg : public CDialog { //メンバ関数//////////////////////////////////////////////////////////////// public: afx_msg void OnBnClickedBtnPrimary(); //プライマリ計算ボタンクリック afx_msg void OnBnClickedBtnSecondary(); //セカンダリ計算ボタンクリック DECLARE_MESSAGE_MAP() }; |
// MultiThreadDlg.cpp : 実装ファイル // // ・ // ・ BEGIN_MESSAGE_MAP(CMultiThreadDlg, CDialog) // ・ ON_BN_CLICKED(IDC_BTN_PRIMARY, &CMultiThreadDlg::OnBnClickedBtnPrimary) ON_BN_CLICKED(IDC_BTN_SECONDARY, &CMultiThreadDlg::OnBnClickedBtnSecondary) END_MESSAGE_MAP() //プライマリ計算ボタンクリック void CMultiThreadDlg::OnBnClickedBtnPrimary() { static int nData = 0; CString strData; strData.Format(_T("%d"), ++nData); GetDlgItem(IDC_STC_PRIMARY)->SetWindowTextW(strData); } //セカンダリ計算ボタンクリック void CMultiThreadDlg::OnBnClickedBtnSecondary() { double dblData = 0; CString strData; GetDlgItem(IDC_STC_SECONDARY)->SetWindowTextW(_T("計算中・・・・")); ::srand( (UINT)::time(NULL) ); for (DWORD i=0; i<30000000; i++) dblData += (double)::rand() / (double)10000; strData.Format(_T("%f"), dblData); GetDlgItem(IDC_STC_SECONDARY)->SetWindowTextW(strData); } |
2と4のボタンのクリック時のハンドラを上記のように実装してください。ビルド後、 4のボタンをクリックして計算している途中で、2のボタンを数回クリックしてください。 結果はあえて書きません。なぜそうなるかはこちら をご覧下さい。
<マルチスレッドでの処理>
では、2のボタンをクリックしてから表示までの演算を ワーカースレッドをつくってそちらに担当させてみます。
// MultiThreadDlg.h : ヘッダーファイル // #pragma once #include <afxmt.h> #define WM_CALCCOMPLETE (WM_USER + 12) //セカンダリスレッド計算終了メッセージ // CMultiThreadDlg ダイアログ class CMultiThreadDlg : public CDialog { // ・ // ・ //メンバ変数//////////////////////////////////////////////////////////////////////////// protected: CWinThread *m_pSecThread; //セカンダリスレッドへのポインタ double m_dblData; //セカンダリスレッド計算用データ CCriticalSection m_critsec; //同期オブジェクト(クリティカルセクション) BOOL m_bAbort; //セカンダリスレッド中止フラグ //メンバ関数//////////////////////////////////////////////////////////////////////////// protected: void EndMultiThreadDlg(int nResult); //セカンダリスレッド終了確認後ダイアログ終了 virtual void OnOK(); virtual void OnCancel(); public: afx_msg void OnBnClickedBtnPrimary(); //プライマリ計算ボタンクリック afx_msg void OnBnClickedBtnSecondary(); //セカンダリ計算ボタンクリック //セカンダリスレッド計算終了メッセージハンドラ afx_msg LRESULT OnCalcComplete(WPARAM wParam, LPARAM lParam); //セカンダリスレッド制御関数 static UINT __cdecl ThreadProc(LPVOID pParam); DECLARE_MESSAGE_MAP() }; |
シングルスレッドのときと同様に4のボタンを押して 「計算中・・・」が表示されたのち、2のボタンを押して計算結果が表示されるか確認してみてください。 次に各メンバの解説を掲載します。
(メンバ変数)
m_pSecThread・・・ | CWinThread型のスレッドオブジェクトへの ポインタです。プライマリスレッドからAfxBeginThreadを発行するとセカンダリスレッドが作成され、 スレッドのハンドルがこの変数に保持されます。 |
m_dblData・・・・・・・ | シングルスレッドの場合は、2のボタンのクリック時のハンドラ内のローカル変数 で済みましたが、今回は計算はセカンダリスレッド(制御関数 ThreadProc)で、表示は プリマリスレッド(OnCalcComplete)で行うためクラスのメンバ変数として定義しました。 また、この変数は両スレッドで共有するので同期の問題が発生します。メッセージで処理している ので実は必要なく蛇足になりますが、使い方の提示という意味で クリティカルセクションでの同期を使っています。 |
m_critsec・・・・・・・ | 上記のm_dblDataが両スレッド間で利用する共有データなので同期をとるために 使うクリティカルセクション変数。ただ、本当はWM_CALCCOMPLETEユーザメッセージを使って データの書き換えを通知しているため、さらにプライマリスレッドではデータの取得オンリー、 セカンダリスレッドでは書き換えオンリーのため、この同期オブジェクトは蛇足に 近いことに注意してください。 |
m_bAbort・・・・・・・ | 4のボタンを押した後、計算中にプライマリスレッドで稼動中のダイアログを
閉じようとして、セカンダリスレッドに計算中止を知らせるフラグ。もし、セカンダリ稼動中
(計算中)に先にプライマリスレッドが閉じてしまった場合、スレッドオブジェクトが
メモリリークするので、ダイアログを閉じることをセカンダリに知らせるためにこの変数を使います。 こちらも、両スレッドで利用する共有データですが、こちらはセカンダリスレッドでリードオンリー なので同期をとっていません。 |
(メンバ関数)
OnBnClickedBtnPrimary・・・ | シングルスレッドの場合はこのハンドラ内で3000万回の乱数の演算を実行して 表示していましたが、今回はこの演算はセカンダリスレッドで行わせるので、ここではスレッドの 作成と制御関数の稼動を促しています。 |
ThreadProc・・・・・・・・・・・・・ | セカンダリスレッドで稼動する制御関数です。AfxBeginThread関数で制御関数に
渡す引数にダイアログへのポインタを指定して、そのメンバのm_dblDataを使って演算します。 (A)のループでかなりの時間を費やしていますが、この処理中にプライマリスレッドのダイアログが 閉じられてしまっては、セカンダリスレッドオブジェクトがメモリリークしてしまってまずいです。 そこで、ダイアログを閉じようとしたらm_bAbortフラグを立てて、ループを中止してスレッド終了を 待ちます。フラグを立てる、セカンダリスレッドの終了を待つ処理はこの後に書く EndMultiThreadDlgで行っています。 演算が完了したら、(B)でプライマリスレッド側にユーザメッセージWM_CALCCOMPLETEをポストして 知らせてスレッドを終了させます。 |
OnCalcComplete・・・・・・・・・ | セカンダリスレッドでm_dblDataの演算が完了したら送られてくるWM_CACLCOMPLETE メッセージハンドラです。共有データm_dblDataをスタティックテキストコントロールに表示して、 セカンダリスレッドが終了するのをWaitForSingleObjectで待ちます。シグナル状態になったら スレッドオブジェクトを閉じています。 |
EndMultiThreadDlg・・・・・・・ | ダイアログを閉じる際にOnOK、OnCancelをオーバーライドして呼び出す関数です。 通常のダイアログの終了処理ではだめで、m_bAbortの解説の繰り返しになりますが、 プライマリスレッドを閉じる前にセカンダリスレッドが稼動しているかどうかを確認して稼動して いたらその中断、終了を待たなければなりません。この実装をEndMultiThreadDlgで行っています。 |