マルチスレッドプログラミング(VB.net編)
マルチスレッドプログラミング(C#編)
もあるので、ご覧あれ。
5秒以上の時間を要する処理をやらせていると応答なしになってかっこ悪い、なんてことをアラフィフ元プログラマーのおいらが言われて、それはそうですよねぇなんて言いつつ、どうしたらいいのかと周囲の人間に聞くも大半の人間はわからず、そのあたりに詳しい人間に聞いたらマルチスレッドと非同期処理の講義を20分も聞かされてしまった。案件がVB.netということもあり、自分で組めるものではなかった。組めないなりにググって要点は把握できた。最終的には、協力会社の何でも書けちゃうプログラマに依頼して、VB.net案件は片が付いた。昔ながらのやり口であるThreadクラスを使う方法で落ち着いたが、過程としてasync/awaitなる最新の非同期処理を案内されるもおいらがさっぱりわからんかったので、おいらが別の人間に説明できるものでもなくあきらめた。
そんな他人依存でプログラム案件をやり過ごしたことが、無性に腹立たしかったため、土曜のくそ朝っぱらからプログラミングに取り組んだ。おいらが書けるのはC++だけなもんだから、近頃の風潮とは合致しないんだけれども、ともあれ書いてみることは重要だ。AfxBeginThreadというキーワードは覚えていたし、昔取った杵柄でもあるからとガッツリ書いてみたなり。
要件は以下の通り。時間がかかる処理をしているあいだに、画面上には過程を表す表示を行い、中止ボタンにより処理を止めることができる。中止ボタンを押すことで、本当に止めてもいいかとメッセージを表示し、メッセージ表示中は処理を一時中断する。もちろん、処理に5秒以上かかっても応答なしにならない。UI設計は以下の通り。
ソースコードを以下に抜き出してみる。ソースだけでは使い勝手悪いだろうから、プロジェクトそのものをここに上げておく。VisualStudio 2010 C++で作ったもの。わかりそうでわからないマルチスレッドについて、いますぐ実装したい人は持っていってくれ。
BOOL CThreadTestDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // このダイアログのアイコンを設定します。アプリケーションのメイン ウィンドウがダイアログでない場合、 // Framework は、この設定を自動的に行います。 SetIcon(m_hIcon, TRUE); // 大きいアイコンの設定 SetIcon(m_hIcon, FALSE); // 小さいアイコンの設定 UpdateMsg( 0 ); // メッセージ初期化 GetDlgItem(IDC_BUTTON2)->EnableWindow(0); // 中止ボタンを無効化 m_stcResult.SetWindowText(_T("")); // 結果文字列をクリア return TRUE; } // 開始ボタン押下 void CThreadTestDlg::OnBnClickedButton1() { GetDlgItem(IDC_BUTTON1)->EnableWindow(0); // 開始ボタンを無効化 GetDlgItem(IDC_BUTTON2)->EnableWindow(1); // 中止ボタンを有効化 GetSystemMenu(FALSE)->EnableMenuItem( SC_CLOSE, MF_GRAYED ); // ×ボタンを無効化 m_stcResult.SetWindowText( _T("") ); // 結果文字列をクリア UpdateMsg( 0 ); // メッセージ初期化 // サブスレッド開始 m_pThread = AfxBeginThread( (AFX_THREADPROC)ExecProc, (LPVOID)this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, NULL); m_pThread->m_bAutoDelete = FALSE; // 自動破棄フラグクリア m_pThread->ResumeThread(); // サスペンド解除 } static UINT ExecProc( LPVOID pParam ) { CThreadTestDlg* pDlg = (CThreadTestDlg*)pParam; pDlg->ExecFunc(); return 0; } void CThreadTestDlg::ExecFunc() { // この関数の中がサブスレッド。サブスレッドからやってはいけないこと。 // ・画面周りのコントロール(メインスレッド)を変更する // ・メインスレッドから変更される可能性がある変数を変更する // ようするに、メインとサブでは一定の独立性を保つべしということ try { for( int i=0; i<10; i++ ) { Sleep(1000); // 時間がかかる処理 if( m_Stop ) // 中止されたフラグを見て、ループを抜ける break; UpdateMsg( i+1 ); } if( m_Stop ) m_strResult = _T("中止された。"); // ここはメンバー変数を使わずに、メッセージパラメータで渡したほうがいいのかも。。 else m_strResult = _T("最後まで実行された。"); } catch(...) { m_strResult = _T("異常終了。"); } PostMessage( WM_APP_THREADEND, 0, 0 ); // サブスレッドが終わったことをメインスレッドに知らせる } // サブスレッドが終わった LRESULT CThreadTestDlg::OnThreadEnd(WPARAM wParam, LPARAM lParam) { if( !m_pThread ) // サブスレッドがスタートしていない(念のためチェック) return 0L; WaitForSingleObject( m_pThread->m_hThread, INFINITE ); // サブスレッドが終了するのを待つ delete m_pThread; m_pThread = NULL; GetDlgItem(IDC_BUTTON1)->EnableWindow(1); // 開始ボタンを有効化 GetDlgItem(IDC_BUTTON2)->EnableWindow(0); // 中止ボタンを無効化 GetSystemMenu(FALSE)->EnableMenuItem( SC_CLOSE, MF_ENABLED ); // ×ボタンを有効化 m_stcResult.SetWindowText( m_strResult ); // 結果を更新 m_Stop = FALSE; return 0L; } // 中止ボタン押下 void CThreadTestDlg::OnBnClickedButton2() { if( !m_pThread ) // サブスレッドがスタートしていない(念のためチェック) return; m_pThread->SuspendThread(); // サブスレッド中断 if( MessageBox(_T("中止しますか?(メッセージ表示中はスレッド中断)"), _T("確認"), MB_YESNO ) != IDYES ) { m_pThread->ResumeThread(); // サブスレッド再開 return; } m_pThread->ResumeThread(); // サブスレッド再開 m_Stop = TRUE; // 中止されたフラグを立てる(サブスレッド側が検知する) UpdateMsg( 0 ); // メッセージ初期化 } void CThreadTestDlg::UpdateMsg( int counter ) { CString msg; msg.Format( _T("時間がかかる処理を実行中 %d/10"), counter ); m_stcMsg.SetWindowText( msg ); }
やってみると、やっぱプログラムはおもしろいなと思う。仕事でプログラムをしなくなってからというものその喜びを忘れてしまい、やれ設計書だ工程表だ、打ち合わせだとつまらんことしてるなぁ。とは言え、昔みたいに頭がさえて根気がある状況でもないので、そんなかったりー仕事のほうがいいのかもしれんな、アラフィフオヤジにとってみればさ。近い将来、C#でasync、awaitの非同期処理にアプローチしてみて、同様のサンプルを作ってここで公開してみましょかね。