Neutral Scent

App developments & Gadgets

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が割り当てられているような気がしてきた。そう考えるとちょっと納得がいく動きという気がする...。