スレッド セーフなコントロールの呼び出し

独自の実装

コントロールをスレッド セーフとするには、次のように呼び出します。

  1. コントロールが破棄されていないか、IsDisposedプロパティで確認する。
  2. コントロールのInvokeRequiredプロパティから、スレッドが異なるか確認する。
  3. コントロールと同一のスレッドで実行されるように処理する。
    • スレッドが異なる … コントロールと同一のスレッドで実行されるように、delegateを使用してInvoke()メソッドから呼び出す。
    • スレッドが同一 … コントロールを直接呼び出す。

破棄されているコントロールへアクセスすると、ObjectDisposedException例外が発生します。

スレッドを考慮をせずコントロールにアクセスすると、「有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'control' がアクセスされました。」としてInvalidOperationException例外が発生します。

コントロールのスレッドでの実行

public object Invoke(
    Delegate method
)
Control.Invoke メソッド (Delegate) (System.Windows.Forms) | MSDN

デリゲートを非同期で実行するならば、代わりにBeginInvoke()で呼び出します。

public IAsyncResult BeginInvoke(
    Delegate method
)
Control.BeginInvoke メソッド (Delegate) (System.Windows.Forms) | MSDN

サンプルコード

// Windowsフォームコントロールに対して非同期な呼び出しを行うためのデリゲート
delegate void SetTextCallback( TextBox textBox, string text );
private void SetText( TextBox textBox, string text )
{
    // コントロールが破棄されているならば、何もしない
    if (textBox.IsDisposed) return;

    // 呼び出し元のコントロールのスレッドと異なるかを確認する
    if( textBox.InvokeRequired )
    {
        // このブロックは、別スレッドで実行される

        // 同一メソッドへのコールバックを作成する
        SetTextCallback delegateMethod = new SetTextCallback( SetText );

        // コントロールの親のInvoke()メソッドを呼び出すことで、
        //  呼び出し元のコントロールのスレッドでこのメソッドを実行する
        this.Invoke( delegateMethod, new object[] { textBox, text } );
    }
    else
    {
        // このブロックは、同一スレッドで実行される

        // コントロールを直接呼び出す
        textBox.Text = text;
    }
}

MethodInvokerを用いればデリゲートの定義を省略でき、これは次のようにも記述できます。

private void SetText(TextBox textBox, string text)
{
    if (textBox.IsDisposed) return;
    if (textBox.InvokeRequired)
    {
        this.Invoke((MethodInvoker)delegate { SetText(textBox, text); });
    }
    else
    {
        textBox.Text = text;
    }
}

BackgroundWorkerによる実装

マルチスレッドによる処理はBackgroundWorkerを用いることで、コントロールへのアクセスが容易になります。このクラスはそれぞれの処理の過程で、次のように異なるスレッドでイベントを発生します。

  1. 処理の開始 (DoWorkイベント) … ワーカースレッド (メインスレッドのコントロールへのアクセス不可)
  2. 処理中 (ProgressChangedイベント) … メインスレッド
  3. 処理の完了 (RunWorkerCompletedイベント) … メインスレッド
private BackgroundWorker backgroundWorker;


{
    this.backgroundWorker = new BackgroundWorker();
    this.backgroundWorker.WorkerReportsProgress = true;

    this.backgroundWorker.DoWork += BackgroundWorker_DoWork;
    this.backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
    this.backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;

    // ...

    int data = 123;
    if (!this.backgroundWorker.IsBusy)
    {
        this.backgroundWorker.RunWorkerAsync(data);
        // このRunWorkerAsyncメソッドの呼び出しによりDoWorkイベントが発生するが、
        //  ハンドラは別スレッドで呼び出されるため、いつ呼ばれるかはわからない
    }
}
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    // このメソッドは、ワーカースレッドで実行される

    BackgroundWorker worker = (BackgroundWorker)sender;
    int data = (int)e.Argument;

    int max = 150;
    for (int i = 0; i < max; i++)
    {
        data++;
        System.Threading.Thread.Sleep(50);

        worker.ReportProgress(100 * i / max);
        // このReportProgressメソッドの呼び出しによりProgressChangedイベントが発生するが、
        //  ハンドラは別スレッドで呼び出されるため、いつ呼ばれるかはわからない

        // WorkerReportsProgressがtrueでないと、
        //  ここでInvalidOperationException例外が発生しRunWorkerCompletedイベントが発生する
    }
    e.Result = data;

    // このDoWorkイベント ハンドラを抜けると、RunWorkerCompletedイベントが発生する
}
private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    Debug.Write(e.ProgressPercentage);
}
private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (e.Error != null)
    {
        // エラーが発生した
    }
    else if (e.Cancelled)
    {
        // キャンセルされた
    }
    else
    {
        int result = (int)e.Result;
        Debug.Write(result);
    }
}

ラムダ式を用いると、これらは次のようにも記述できます。

backgroundWorker = new BackgroundWorker();
backgroundWorker.WorkerReportsProgress = true;

backgroundWorker.DoWork += (object sender, DoWorkEventArgs e) => { };
backgroundWorker.ProgressChanged += (object sender, ProgressChangedEventArgs e) => { };
backgroundWorker.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) => { };

if (!this.backgroundWorker.IsBusy)
{
    this.backgroundWorker.RunWorkerAsync();
}
MSDN (Microsoft Developer Network) から検索