くるくる君


やあみんな、ついに春がやって来たね。今朝なんかはおじさんの通勤路の川沿いの桜も満開になっていたけどみんなのまわりではどんな感じかな。
何か重たい計算処理をしているときや、単にネットワークの向こうからの返答を待っているブロッキング状態でアプリケーションの処理はおろか、表示やボタンなどUIの反応も止まってしまうのはとても残念なことだよな。
全てコンピュータの右手一本で処理をさせているので、ある重たい計算処理からなかなか抜け出せないがために、GUI応答処理そのものであるメッセージループに戻ることができない状態だ。もちろん右手とはメインスレッドのことだ。
ならば重い計算処理や待つだけのブロッキング処理はコンピュータの左手にやらせてしまおうというのがワーカスレッドを作るやり方だ。
重たい処理をワーカスレッドでやるようにすれば、メインスレッドのUIの方はユーザー応答にいつでも反応するボタン(例えば「中止ボタン」)などを提供できるようになるだけではなく、重たい処理を今裏で行ってますよという、何か模様とかがくるくる回っているような、描画処理を実行させることができるので、ビジー処理稼動中であることを知らせ、ユーザーを安心させることもできる。

今回はこれをMFCのダイアログアプリで実現させた実装の一例だ。このやり方を一言で言ってしまうと、「ワーカスレッドによって変更される処理完了フラグをメインGUIスレッドのアイドリング処理で監視する」となる。具体的には、

  1. ボタンが押されたら処理が終ったリょーフラグをオフにし、ワーカスレッドを作成、そのスレッド関数の中で目的の重たい処理を走らせる。
  2. それと同時に、タイマーを起動する。タイマーPROCの中では単にくるくるまわる描画オブジェクトの更新を促す。
  3. メインスレッドのアイドリング処理の中で絶えず処理が終ったりょーフラグを監視させ、もしフラグがONになっていたなら、然るべき処理(例えば結果の表示とか、くるくるのタイマー止めるとか)をするようにする。
  4. ワーカスレッドの処理が終ったならば、処理が終ったりょーフラグをONにする。

そう、このやり方のポイントは処理が終ったりょーフラグだ。ワーカスレッドとメインスレッドとはそれぞれ非同期にめいめい動いているため、ワーカスレッドの処理が終ったタイミングで、直接UIの更新をすることはできない。そこで処理が終ったりょーフラグをメインスレッドのアイドリングで監視しようというわけさ。
スレッドを起動する処理は例えば以下のようになる。

unsigned int __stdcall lc_hi( void* pdata )
{
  // ここに、時間のかかるコア処理を書く。
  Sleep( 8000 );  
  // 終ったなら、IDLEに監視させている終ったよフラグを立てる。
  ChelloIdleDlg* dlg = (ChelloIdleDlg*)pdata;
  dlg->_is_finished = !0;// 処理が終ったりょーフラグ
  return 0;
}

void ChelloIdleDlg::OnBn1Clicked()
{
  // TODO: ここにコントロール通知ハンドラ コードを追加します。
  this->_is_finished = 0;// 処理が終ったりょーフラグ
  this->_handle = (HANDLE)_beginthreadex(NULL, 0, lc_hi, this, 0, NULL);
  SetTimer(1,20,NULL);  // タイマーで、愉快な絵を描画更新しよう。
  return;
}

まあポイントは、_beginthreadex()呼び出しの前に、処理が終ったりょーフラグを0にしており、かたやスレッド関数内部の重たい処理の内容(ここではSleep( 8000 );を重たい処理だと思って欲しい)の終了時にこのフラグをセットしてやっているということだ。
さて次はこのフラグを監視するアイドリング処理を記述するよ。

LRESULT ChelloIdleDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  // TODO: ここに特定なコードを追加するか、もしくは基本クラスを呼び出してください。
  if( message == WM_KICKIDLE )
  {
    // here is your idle code
    if( this->_is_finished )
    {
      this->KillTimer( 1 );
      CloseHandle( this->_handle ); /* ハンドルを閉じる */
    }
  }
  return CDialog::WindowProc(message, wParam, lParam);
}

クラスのプロパティからWindowProc()関数をオーバーライドし、WM_KICKIDLEが来たときにのみこの処理が終ったりょーフラグのチェックを行う。処理が終ったリょーフラグがONになっていたなら、ワーカスレッドの処理は完了してるので、スレッド処理をクローズさせ、タイマーを閉じる。
さあ、それでは残ったのはタイマー処理だけどこれはどうでもいいねぶっちゃけ。くるくる回っているように見えるテキストを交互に表示してもよいわけだし。まあ一応今回はOpenGLでくるくるまわるやつをあれしたので参考にしてくれ。

void ChelloIdleDlg::OnTimer(UINT_PTR nIDEvent)
{
  // TODO: ここにメッセージ ハンドラ コードを追加するか、既定の処理を呼び出します。

  // テキストくるくるならこういうの
  static char rot[][5]={"|", "/", "-", "v" };
  static int cnt=0;
  this->_static1.SetWindowText( CString(rot[cnt]) );
  ++cnt;
  if( cnt==3 )
    cnt = 0;

  // OpenGL ならこういう感じ。
  this->_angle += 20;
  this->Invalidate( 0 );// OnPaintの中でgl描画。

  CDialog::OnTimer(nIDEvent);
}