スレッドから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 )文字列 );
プロシージャ
case WM_USER_SETTEXT:
    SetWindowText( ( HWND )wp, ( TCHAR* )lp );
    break;
  • SetWindowTextだけでなく、SetWindowPosなどもスレッド側から呼び出すと危険
  • 結局、C#invoke的なものが必要か
  • 他のプログラマさんのスレッドを使ったストップウォッチ実装例が見てみたい。スレッドの基礎ですよね??ググってもサンプルがあまり見つからない。。。
  • スレッドの実行関数は、Controllerに持たせるべきなのか。Model?View?