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

独自の実装

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

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

※1 破棄されているコントロールからInvoke()を呼び出すと、「破棄されたオブジェクトにアクセスできません。」としてObjectDisposedException例外が発生します。

※2 異なるスレッドからコントロールにアクセスすると、「有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'control' がアクセスされました。」として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.IsDisposed) return;

    // 呼び出し元のコントロールのスレッドと異なるかを確認する
    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.IsDisposed) return;
    if (control.InvokeRequired)
    {
        control.Invoke((MethodInvoker)delegate { SetText(text); });
    }
    else
    {
        control.Text = text;
    }
}

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

if (!control.IsDisposed)
{
    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;
});

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スレッド以外からは使用できません。

// ここはUIスレッド
Task.Run(() =>
{
    // ここはTaskのスレッド
    Task.Run(() =>
    {
        //
    }).ContinueWith((_task) =>
    {
        //
    }, TaskScheduler.FromCurrentSynchronizationContext()); // InvalidOperationException「現在の SynchronizationContext を TaskScheduler として使用することはできません。」
});
Microsoft Learnから検索