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

Control.Invoke()メソッド

Control.Invoke()を用いることでスレッド セーフとできますが、スレッドが同一ならばこのメソッドを介する必要はないため、InvokeRequiredで確認した上で呼び出します。

  • スレッドが異なる … コントロールと同一のスレッドで実行されるように、delegateを使用してInvoke()※1 から呼び出す。※2
  • スレッドが同一 … コントロールを直接呼び出す。

※1 破棄されているコントロールからInvoke()を呼び出すと、「破棄されたオブジェクトにアクセスできません。」としてObjectDisposedExceptionが投げられます。コントロールが破棄されていることはIsDisposedで事前に確認できますが、確認してからInvoke()を呼ぶまでの間に他のスレッドで破棄される恐れがあるため、ObjectDisposedExceptionを捕捉するのが確実です。

※2 異なるスレッドからコントロールにアクセスすると、「有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'control' がアクセスされました。(Cross-thread operation not valid: Control 'control' accessed from a thread other than the thread it was created on.)」としてInvalidOperationExceptionが投げられます。このときの「コントロールが作成されたスレッド」とはコントロールがnewで作成されたスレッドではなく、コントロールのハンドルが作成されたスレッドになります。

コントロールのハンドル

通常、コントロールにハンドルが関連付けられているか、Control.IsHandleCreatedで確認する必要はありません。この確認はControl.InvokeRequired内で行われ、関連付けられていないならば親コントロールのハンドルからスレッドが判定されます。InvokeRequired - Control.cs

ただし親コントロールに追加していない、つまりControl.Parentの上位の階層までnullとなるコントロールは注意が必要です。そのときControl.InvokeRequiredはfalseを返すため、結果として異なるスレッドからアクセスすることになりますがInvalidOperationExceptionは投げられません。この状況を回避するには、コントロールの属するフォームのLoadイベントが発生するまで、バックグラウンドのスレッドを作成するのを待機します。Remarks - Control.InvokeRequired Property (System.Windows.Forms) | Microsoft Learn

サンプルコード

// Windowsフォームコントロールに対して非同期な呼び出しを行うためのデリゲート
delegate void SetTextCallback( string text );
private void SetText( string text )
{
    // 呼び出し元のコントロールのスレッドと異なるかを確認する
    if (control.InvokeRequired)
    {
        // このブロックは、別スレッドで実行される

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

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

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

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

private void SetText(string text)
{
    if (control.InvokeRequired)
    {
        control.Invoke((MethodInvoker)delegate { SetText(text); });
    }
    else
    {
        control.Text = text;
    }
}

またはMethodInvokerのメソッドをローカルに定義すれば、再帰呼び出しすることなく記述できます。

MethodInvoker method = () =>
{
    // コントロールに対する処理
};

if (control.InvokeRequired)
{
    control.Invoke(method);
}
else
{
    method();
}

別スレッドから呼ばれることが確実ならば、InvokeRequiredを省略してデリゲートのメソッド内に記述できます。

control.Invoke((MethodInvoker)delegate
{
    // コントロールに対する処理
});

戻り値があるならば、Invoke()に渡すデリゲートをFunc<TResult>などにします。

int result = (int)control.Invoke((Func<int>)delegate
{
    return 1;
});

InvokeRequiredにより分岐する処理は、次のように拡張メソッドにまとめられます。c# - Automating the InvokeRequired code pattern - Stack Overflow

public static void InvokeIfRequired(this Control control, Action method)
{
    if (control.InvokeRequired)
    {
        control.Invoke(method);
    }
    else
    {
        method();
    }
}
public static T InvokeIfRequired<T>(this Control control, Func<T> method)
{
    if (control.InvokeRequired)
    {
        return (T)control.Invoke(method);
    }
    else
    {
        return method();
    }
}

BackgroundWorkerクラス

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

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

サンプルコード

private BackgroundWorker backgroundWorker;


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

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

    // ...

    int data = 123;
    if (!backgroundWorker.IsBusy)
    {
        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)
{
    Console.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;
        Console.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 (!backgroundWorker.IsBusy)
{
    backgroundWorker.RunWorkerAsync();
}

SynchronizationContextクラス

スレッド セーフな処理を、Windows Form以外にも適用できます。ただし実行環境によって、動作に相違があります。SynchronizationContext の実装に関する注意事項 - MSDN マガジン: 並列コンピューティング - SynchronizationContext こそすべて | Microsoft Learn

Task.ContinueWith()メソッド

Task.ContinueWith()にTaskScheduler.FromCurrentSynchronizationContext()で取得できるTaskSchedulerを渡すことで、UIが作成されたのと同じスレッドで指定の処理を実行できます。

// ここはUIスレッド
Task.Run(() =>
{
    //
}).ContinueWith((_task) =>
{
    // ここもUIスレッド
}, TaskScheduler.FromCurrentSynchronizationContext());

ただしこの方法は、UIスレッド以外からは使用できません。呼び出すと「現在の SynchronizationContext を TaskScheduler として使用することはできません。」としてInvalidOperationExceptionが投げられます。

// ここはUIスレッド
Task.Run(() =>
{
    // ここはTaskのスレッド
    Task.Run(() =>
    {
        //
    }).ContinueWith((_task) =>
    {
        //
    }, TaskScheduler.FromCurrentSynchronizationContext()); // InvalidOperationException
});
Microsoft Learnから検索