VC.NET ~ System::Threading::Timerは急に止まれない

前置き

.NETには3種類のタイマークラスがあります。すなはち、①System::Windows::Forms::Timer、②System::Timers::Timer、③System::Threading::Timerです。

①はフォームとの相性が良いのが特徴で、タイムアウトをトリガにしてフォーム上のコントロールを変化させるときに重宝します。ただ、メインスレッド(UIスレッド)がビジーなときにタイマー精度が落ちるのが難点です。

で、比較的高精度のタイマーが欲しい時は②か③を使うわけですが、その使い分けは良く分かりません。個人的には②が好きです。というのも、③でハマッた経験があるからでして、本記事では、その辺のことを書きます。

本文

いきなり例外発生

フォームのコンストラクタで、System::Threading::Timerをセットアップします。タイムアウトイベントはワーカースレッド上で発生するので、UIを変更するためにBeginInvoke()でワンクッションかましてます。

private delegate void TimedOutDele(void);

public ref class ComboBoxForm : public System::Windows::Forms::Form
{
public:
  ComboBoxForm(void)
  {
    InitializeComponent();

    mDele = gcnew TimedOutDele(this, &ComboBoxForm::timedOutMain);
    mClockLabel->Text = "";
    mTimer = gcnew System::Threading::Timer(
      gcnew System::Threading::TimerCallback(this, &ComboBoxForm::timedOut),
      nullptr,   // (A)
      0,         // 最初のタイムアウトまでの時間(msec)
      1000);     // その後のタイムアウト間隔(msec)
  }

private:
  System::Threading::Timer^ mTimer;
  TimedOutDele^ mDele;

  void timedOutMain() {
    mClockLabel->Text = DateTime::Now.ToString();
  }

  void timedOut(Object^ state) {
    BeginInvoke(mDele);
  }

...

しかし、このままでは、最初のタイムアウト時のBeginInvoke()で例外System::InvalidOperationExceptionが発生します。

'System.InvalidOperationException' のハンドルされていない例外が
System.Windows.Forms.dll で発生しました。

追加情報: ウィンドウ ハンドルが作成される前、コントロールで Invoke または BeginInvoke を
呼び出せません。

別にThreading::Timerが悪いわけではありません。どうも、フォームやコントロールのウィンドウハンドルの生成にはdelayがあるようです。フォームのHandleプロパティを参照することにより、ウィンドウハンドル生成を同期化することができます。

  // Threading::Timerをgcnewする前で。
  // ウィンドウハンドル生成を促すため、Handleプロパティを参照する。
  int i = Handle.ToInt32();

これで、ラベルに日時を表示することができました。

フォームを閉じたときに例外発生

この後、フォームを閉じると、またもや例外を食らってしまいました。Threading::TimerはIDisposableを継承しているので、使い終わったらディスポーズしてやらないといけません。フォームを閉じるときに、タイマーを停止してディスポーズしましょう。

private: System::Void ComboBoxForm_FormClosing(
        System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e) {
  mTimer->Change(System::Threading::Timeout::Infinite, System::Threading::Timeout::Infinite);
  delete mTimer;
  mTimer = nullptr;
}

Invoke()に変えると、また例外

実は、上記のタイマー停止処理では不十分みたいです。Threading::Timerはワーカースレッド上で動いているので、メインスレッド上でFormClosingイベントが発生した時点で、既にタイムアウトイベントがキューに貯まっているかもしれません。その場合、FormClosingの後でタイムアウトハンドラが呼ばれる可能性があります。

上記のコードで数十回試した限りでは大丈夫でしたが、試しに、タイムアウトハンドラ内で使っているBeginInvoke()をInvoke()に変えてみたところ、あっさり例外が発生しました。BeginInvoke()でも、タイミングによっては危ないかもしれません。

となると、何らかの状態を保持して、タイムアウトハンドラの中で状態を判定する必要がありそうです。しかし、これには排他制御が絡むので厄介です。たかがタイマーを停止するだけのことに、そこまで必要とは…。

そういうわけで、特に理由が無いならThreading::Timerの代わりにSystem::Timers::Timerを使いましょう。

System::Timers::Timer

Timers::Timerも、Threading::Timerと同様、ワーカースレッド上で動きます。しかし、親切なことに、メインスレッドとの同期の機能もサポートしてくれているのです。それには、SynchronizingObjectプロパティを使います。

  ComboBoxForm(void)
  {
    InitializeComponent();

    mClockLabel->Text = "";

    mTimer = gcnew System::Timers::Timer(1000);
    mTimer->SynchronizingObject = mClockLabel;
    mTimer->Elapsed += gcnew System::Timers::ElapsedEventHandler(this, &ComboBoxForm::timedOut);
    mTimer->Start();
  }

private:
  System::Timers::Timer^ mTimer;

  void timedOut(Object^ sender, System::Timers::ElapsedEventArgs^) {
    mClockLabel->Text = DateTime::Now.ToString();
  }

...

このように、SynchronizingObjectプロパティにコントロールやフォームをセットしておけば、タイムアウトハンドラ(Elapsedイベント)は、メインスレッド上でコールバックされます。厳密に言えば、そのコントロールやフォームを生成したスレッド上でコールバックされます。

まぁ、恐らくTimers::Timerも、内部ではBeginInvoke()を使ってるんだとは思いますが、デリゲートが1個少なくて済むということを考えても、Timers::Timerを使わない手はないでしょう。また、Timers::TimerはIDisposableを継承してないので、ディスポーズ(つまりdelete)は不要です。

Last modified:2012/03/09 01:25:28
Keyword(s):
References:[.NETアプリ開発]
This page is frozen.