- PR -

BackgroundWorkerでTimerをStop, Startさせた場合の動作

1
投稿者投稿内容
KI
大ベテラン
会議室デビュー日: 2007/01/10
投稿数: 239
投稿日時: 2007-03-27 13:21
VS2005 C# Windowsフォームのアプリケーションです。

フォームが開いている間、一定間隔で行う処理があるため、
Timer(System.Windows.Forms.Timer)を用いて処理を行っています。
また、フォーム上でのある操作をトリガとして起動される別のタスクがありますが、
実行中もUIを応答させたいので、このタスクをバックグラウンドで実行するために、
BackgroundWorker を使用しています。

ここで、BackgroundWorkerで実行されるタスクを実行中は
タイマーの処理を止めたいので、DoWorkのイベントハンドラ内で
タイマーをStopして、処理を行った後Startさせています。

再現コードを以下に示しますが、これを実行すると、
フォーム起動したときは1秒間隔で「Tick」が出力されるのですが、
ボタンを押して BackgroundWorker のタスクを実行すると、
タスクの終了後、「Tick」が全く出力されなくなります。
タイマーの Enabled は true になっているにも関わらずです。

解決策は見つかっていて、 timer1.Stop をRunWorkerAsync の呼び出し前に、
timer1.Start を RunWorkerCompleted のハンドラ内に書けば正常に動作しました。
ただ、なぜこのコードで動作しないのかが納得できないので、
原因のわかる方がいましたら教えてください。

よろしくお願いします。

コード:

// Form2.cs
using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace WindowsApplication1
{
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}

// フォームロード
private void Form2_Load(object sender, EventArgs e)
{
// タイマ起動
timer1.Start();
}

// timer1 の Tick イベントハンドラ
private void timer1_Tick(object sender, EventArgs e)
{
Console.WriteLine("Tick");
}

// backgroundWorker1 の DoWork イベントハンドラ
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Console.WriteLine("DoWork");

// タイマーをとめて
timer1.Stop();

// 5秒待機
System.Threading.Thread.Sleep(5000);

// タイマー再開
timer1.Start();

}

// backgroundWorker1 の RunWorkerCompleted イベントハンドラ
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Console.WriteLine("RunWorkerCompleted");
}

// button1 の Click イベントハンドラ
private void button1_Click(object sender, EventArgs e)
{
backgroundWorker1.RunWorkerAsync();
}
}
}


// Form2.Designer.cs
namespace WindowsApplication1
{
partial class Form2
{
/// <summary>
/// 必要なデザイナ変数です。
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// 使用中のリソースをすべてクリーンアップします。
/// </summary>
/// <param name="disposing">マネージ リソースが破棄される場合 true、
/// 破棄されない場合は false です。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Windows フォーム デザイナで生成されたコード

/// <summary>
/// デザイナ サポートに必要なメソッドです。このメソッドの内容を
/// コード エディタで変更しないでください。
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.timer1 = new System.Windows.Forms.Timer(this.components);
this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
this.button1 = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// timer1
//
this.timer1.Interval = 1000;
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
//
// backgroundWorker1
//
this.backgroundWorker1.DoWork +=
new System.ComponentModel.DoWorkEventHandler(this.backgroundWorker1_DoWork);
this.backgroundWorker1.RunWorkerCompleted +=
new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);
//
// button1
//
this.button1.Location = new System.Drawing.Point(73, 57);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(75, 23);
this.button1.TabIndex = 0;
this.button1.Text = "button1";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// Form2
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(292, 266);
this.Controls.Add(this.button1);
this.Name = "Form2";
this.Text = "Form2";
this.Load += new System.EventHandler(this.Form2_Load);
this.ResumeLayout(false);

}

#endregion

private System.Windows.Forms.Timer timer1;
private System.ComponentModel.BackgroundWorker backgroundWorker1;
private System.Windows.Forms.Button button1;
}
}




[ メッセージ編集済み 編集者: KI 編集日時 2007-03-27 15:29 ]
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2007-03-27 13:39
RunWorkerCompleted で、e.Error の値をチェックしてみては?
KI
大ベテラン
会議室デビュー日: 2007/01/10
投稿数: 239
投稿日時: 2007-03-27 14:06
お返事ありがとうございます。

デバッガで確認してみましたが、e.Error は null でした。
なちゃ
ぬし
会議室デビュー日: 2003/06/11
投稿数: 872
投稿日時: 2007-03-27 14:49
コントロールはUIのスレッドから操作しなければならないという制約ですね。
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2007-03-27 15:19
引用:

デバッガで確認してみましたが、e.Error は null でした。



あれ?予想と違う結果が。。。

System.Windows.Timer なんてものは無い筈なので、System.Windows.Forms.Tiemr を使っているんですよね?

こいつはコントロールではなく Component なので、「コントロールは、その実体を生成したスレッド以外から操作してはならない」という制約には引っかからないような気もするんですが。。。

念のため DoWork() 内から操作する時は Form.Invoke() してみるとか。
KI
大ベテラン
会議室デビュー日: 2007/01/10
投稿数: 239
投稿日時: 2007-03-27 15:30
なちゃさん、渋木さん。お返事ありがとうございます。

私も「コントロールはUIのスレッドから操作しなければならない」ことは把握していました。
そもそも .NET 2.0 ではこれをやるとInvalidOperationException が発生します。
ですが、Timer は Component なので大丈夫だと思っていましたし、
例外もスローされませんでしたので、これが原因とは考えませんでした。

試しにInvokeを使って以下のように書き直したところ正常に動作しました。

コード:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    Console.WriteLine("DoWork");
    
    // タイマーをとめて
    Invoke((MethodInvoker)delegate()
    {
        timer1.Stop();
    });
    
    // 5秒待機
    System.Threading.Thread.Sleep(5000);

    // タイマー再開
    Invoke((MethodInvoker)delegate()
    {
        timer1.Start();
    });
    
}



いずれにせよ Timer オブジェクトが、Enabled が true のまま
Tick イベントを発生させないような状態になるくらいなら、
不正な操作が行われた時点で InvalidOperationException をスローすべきと思うのですが…

引用:
System.Windows.Timer なんてものは無い筈なので、System.Windows.Forms.Tiemr を使っているんですよね?


ご指摘のとおり、System.Windows.Forms.Tiemr の誤りでした。元記事修正しておきました。
ya
大ベテラン
会議室デビュー日: 2002/05/03
投稿数: 212
投稿日時: 2007-03-27 15:46
MSDN ライブラリより引用

引用:


Timer は、ユーザー定義の間隔でイベントを発生させるために使用されます。この Windows タイマは、UI スレッドを使用して処理を実行するシングルスレッド環境に合わせて設計されています。ユーザー コードには利用できる UI メッセージ ポンプが必要です。また、このコードは必ず同じスレッドから操作し、別のスレッドに対する呼び出しをマーシャリングする必要があります。



というわけで、エラーが出ないかつイベントが発生しないのが正常動作ですので(そのスレッドで Application.Run してあげれば発生しますよたぶん)。

とりあえず調べようよという話はおいておいて、実装の話で考えてみると、どうせWM_TIMERを使用していると思われるので、どのスレッドでも初期化可能で、かつそのスレッドにおいてメッセージループが開始されていないと発生しない(開始されてから発生する)というのはまぁ妥当な実装かと。
KI
大ベテラン
会議室デビュー日: 2007/01/10
投稿数: 239
投稿日時: 2007-03-27 16:53
yaさん、ありがとうございます。

最初にMSDN読むべきでしたね…スッキリしました。
System.Windows.Forms.Timer はメッセージポンプがないと動作しないのですね。
それならば、この実装も納得できます。
1

スキルアップ/キャリアアップ(JOB@IT)