スレッドからSendMessageしてはいけない(場合がある)。
↑のような、スレッドを使ったストップウォッチ(StopWatch)を作っています。
- Start/Stopボタン
- スレッドの開始/停止
- Resetボタン
- 表示0、スレッド停止
- スレッド
- 経過時間を表示
MVCのControllerは概ね以下のような処理になると思います。
class StopWatchController { private: StopWatchModel* model; StopWatchView* view; public: //WM_COMMAND int command( windows::CommandEvent& e ) { if( e.id == Start/Stopボタン ) { model->process_event( Start/Stopイベント ); view->update(); } if( e.id == Resetボタン ) { model->process_event( Resetイベント ); view->update(); } return 0; } //スレッドの実行関数 void run() { while( /*...*/ ) { model->update(); view->update(); } } };
MVCのViewでは、Modelから時間を取得してラベルを更新しています。
- Label::set_textでSetWindowText()が実行される。
- さらに、SetWindowText内でSendMessageが実行される
class StopWatchView { private: StopWatchModel* model; windows::Button start_stop; windows::Button reset; windows::Label time; windows::Label state; public: void update() { time.set_text( model->get_time() ); state.set_text( model->get_name() ); } };
スレッドから共有データにアクセスする場合、排他制御が必要になるので、
以下のようにMutexを使ったlock処理を追加します。
//WM_COMMAND int command( windows::CommandEvent& e ) { scoped_lock lock( m ); if( e.id == Start/Stopボタン ) { model->process_event( Start/Stopイベント ); view->update(); } if( e.id == Resetボタン ) { model->process_event( Resetイベント ); view->update(); } return 0; } //スレッドの実行関数 void run() { while( /*...*/ ) { scoped_lock lock( m ); model->update(); view->update(); } }
このプログラムを実行すると「デッドロック」でプログラムがフリーズします。
スレッド+排他制御+SendMessageの組み合わせが危険
- 1.スタートボタンを押してスレッド開始
- 2.スレッドでmutexの所有権獲得
- 3.SetWindowTextの呼び出し
- 3-1.SendMessage(...,WM_SETTEXT, ...)の呼び出し
- 3-2.ウィンドウプロシージャの呼び出し
- 3-3.テキストの変更が完了するまで、呼び出し元スレッド停止
- 4.停止ボタンを押す
- 5.mutexの所有権はスレッドが持っているので、プロシージャ(メインスレッド)が停止
- 6デッドロック!
解決方法:PostMessageを使う
スレッドからのウィンドウ制御は、確実にメインスレッドで行われるようにするためにPostMessageを使うと良いそうです。
ただし、WM_SETTEXTはSendMessages専用なので、ユーザ定義メッセージを使います。
#define WM_USER_SETTEXT ( WM_USER + 1 )
スレッド
PostMessage( 親ウィンドウ, WM_USER_SETTEXT, ( WPARAM )コントロールのID, ( LPARAM )文字列 );