WinFormsとTimerとThreadingと
例えば、こんなアプリケーションがあったとします。
DataSetにデータを蓄えて、DataViewで絞り込んだ複数のビュー(UI)があり、データの更新に合わせてぐわわっと複数の更新メソッドが走る。
DelegateやDataBindingsってのは便利なもので、この手のアプリが非常に簡単に作れます。
ところが、そこに落とし穴。
複数のデータ更新がが連続発生したとき、UIをばしばしと更新されるのは鬱陶しいので、データの更新からUIの更新までにTimerで遅延処理を入れて、複数の更新をひとまとめにするような処理を入れたとします。
このとき、一つのデータを表示する複数のViewが同時に更新を行おうとした場合、一つしかないUIスレッドの持つ複数のSystem.Windows.Forms.Timerの動作がすちゃらかになるようです。
具体的には、他のTimerのTickによって、より早くイベントが発生したり、そもそもTickイベントが発生しなかったリします。(WM_TIMERはちゃんとID振ればそんなコト無かったはずなんだけど、どういう実装になっているのやら...)
例えば、こんなコードで、どーにもTickが来ない...、なんてことになる、と。
private const int INT_UPDATE_INTERVAL = 500; private System.Windows.Forms.Timer timerUpdate = new System.Windows.Forms.Timer(); ctor() { timerUpdate.Interval = INT_UPDATE_INTERVAL; timerUpdate.Tick += new EventHandler(timerUpdate_Tick); } private delegate void UpdateHandler(); public void UpdateListWithTimer() { if (viewset == null) return; // 非UIスレッド用デリゲート呼び出し if (this.InvokeRequired) { this.Invoke(new UpdateHandler(UpdateListWithTimer)); return; } StartTimer(); } private void StartTimer() { timerUpdate.Stop(); timerUpdate.Start(); } private void StopTimer() { timerUpdate.Stop(); } void timerUpdate_Tick(object sender, EventArgs e) { System.Diagnostics.Debug.Assert(!InvokeRequired); StopTimer(); DoUpdate(); }
どーも、おかしい...。と思っていたらTimer.Stop()メソッドのリファレンスにこんな「メモ」が...。
メモ
すべての Timer コンポーネントはメインのアプリケーション スレッド上で動作するため、Windows フォーム アプリケーション内のいずれかの Timer に対して Stop を呼び出すと、アプリケーション内の直ちに処理する必要のある他の Timer コンポーネントからメッセージが表示される場合があります。たとえば、2 つの Timer コンポーネントがあり、1 つは 700 ミリ秒、もう 1 つは 500 ミリ秒に設定されている場合、1 つ目の Timer に対して Stop を呼び出すと、2 つ目のコンポーネントのイベント コールバックが先に受信される場合があります。この動作によって問題が生じる場合は、System.Threading 名前空間の Timer クラスを使用します。
そんなわけで、System.Threading.Timerに書き換えるとこんな感じ。
(UpdateListWithTimer()は変更せず)
private System.Threading.Timer thtimerUpdate; ctor() { thtimerUpdate = new System.Threading.Timer(new System.Threading.TimerCallback(UpdateByTimer), null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); } private void UpdateByTimer(object o) { StopTimer(); DoUpdate(); } private void StartTimer() { thtimerUpdate.Change(INT_UPDATE_INTERVAL, System.Threading.Timeout.Infinite); } private void StopTimer() { thtimerUpdate.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); }
あまり深く追ってませんけど、非同期にTimer呼び出すときは、System.Windows.Forms.Timerは使わない方がよさげです。
参考:
.NET TIPS
タイマにより一定時間間隔で処理を行うには?(スレッド・タイマ編)
http://www.atmarkit.co.jp/fdotnet/dotnettips/373threadtimer/threadtimer.html
-
-
- -
-
追記:
なんとなく、一つのコントロールクラスには同じTimerIDが割り当てられているような気がしてきた。そう考えるとちょっと納得がいく動きという気がする...。