コントロールのスレッドセーフな呼び出しを行う方法 (Windows フォーム .NET)

マルチスレッドで Windows フォーム アプリのパフォーマンスを向上させることができますが、Windows フォーム コントロールへのアクセスは本質的にスレッドセーフではありません。 マルチスレッドによって、ご自分のコードが深刻で複雑なバグにさらされる可能性があります。 2 つ以上のスレッドでコントロールを操作することで、コントロールが一貫性のない状態になり、競合状態、デッドロック、フリーズまたはハングが発生する可能性があります。 アプリにマルチスレッドを実装する場合は、クロススレッド コントロールをスレッドセーフな方法で呼び出すようにします。 詳細については、「マネージド スレッド処理のベスト プラクティス」を参照してください。

重要

.NET 7 と .NET 6 用のデスクトップ ガイド ドキュメントは作成中です。

コントロールを作成していないスレッドから Windows フォーム コントロールを安全に呼び出すには、2 つの方法があります。 System.Windows.Forms.Control.Invoke メソッドを使用してメイン スレッドに作成されるデリゲートを呼び出し、次に、コントロールが呼び出されるようにします。 または、System.ComponentModel.BackgroundWorker を実装します。これによって、バックグラウンド スレッドで実行された作業を結果のレポートから分離するためにイベントドリブン モデルが使用されます。

安全でないクロススレッド呼び出し

コントロールを作成していないスレッドから直接コントロールを呼び出すことは安全ではありません。 次のコード スニペットは、System.Windows.Forms.TextBox コントロールへの安全でない呼び出しを示しています。 Button1_Click イベント ハンドラーにより、新しい WriteTextUnsafe スレッドが作成され、これにより、メイン スレッドの TextBox.Text プロパティが直接設定されます。

private void button1_Click(object sender, EventArgs e)
{
    var thread2 = new System.Threading.Thread(WriteTextUnsafe);
    thread2.Start();
}

private void WriteTextUnsafe() =>
    textBox1.Text = "This text was set unsafely.";
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim thread2 As New System.Threading.Thread(AddressOf WriteTextUnsafe)
    thread2.Start()
End Sub

Private Sub WriteTextUnsafe()
    TextBox1.Text = "This text was set unsafely."
End Sub

Visual Studio デバッガーは InvalidOperationException を「有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロールがアクセスされました。」のメッセージとともに呼び出すことで、安全でないスレッド呼び出しを検出します。InvalidOperationException は、Visual Studio でデバッグ中の安全でないクロススレッド呼び出しでは常に発生し、アプリの実行時でも発生する可能性があります。 この問題は修正が必要ですが、Control.CheckForIllegalCrossThreadCalls プロパティを false に設定することで例外を無効にすることができます。

安全なクロススレッド呼び出し

次のコード例は、作成されていないスレッドから Windows フォーム コントロールを安全に呼び出す 2 つの方法を示しています。

  1. System.Windows.Forms.Control.Invoke メソッド。コントロールを呼び出すメイン スレッドからデリゲートが呼び出されます。
  2. System.ComponentModel.BackgroundWorker コンポーネント。イベントドリブン モデルが提供されます。

どちらの例でも、バックグラウンド スレッドは 1 秒間スリープして、そのスレッドで実行されている作業をシミュレートします。

例: Invoke メソッドを使用する

次の例は、Windows フォーム コントロールへのスレッドセーフな呼び出しを保証するパターンを示しています。 System.Windows.Forms.Control.InvokeRequired プロパティに対してクエリが行われ、コントロールの作成スレッド ID が呼び出しスレッド ID と比較されます。 異なる場合は、Control.Invoke メソッドを呼び出す必要があります。

WriteTextSafe を使用すると、TextBox コントロールの Text プロパティを新しい値に設定できます。 メソッドにより、InvokeRequired のクエリが行われます。 InvokeRequiredtrue を返す場合、WriteTextSafe は、それ自体を再帰的に呼び出し、メソッドをデリゲートとして Invoke メソッドに渡します。 InvokeRequired から false が返される場合、WriteTextSafe により、TextBox.Text が直接設定されます。 Button1_Click イベント ハンドラーにより、新しいスレッドが作成され、WriteTextSafe メソッドが実行されます。

private void button1_Click(object sender, EventArgs e)
{
    var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); });
    var thread2 = new System.Threading.Thread(threadParameters);
    thread2.Start();
}

public void WriteTextSafe(string text)
{
    if (textBox1.InvokeRequired)
    {
        // Call this same method but append THREAD2 to the text
        Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); };
        textBox1.Invoke(safeWrite);
    }
    else
        textBox1.Text = text;
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim threadParameters As New System.Threading.ThreadStart(Sub()
                                                                 WriteTextSafe("This text was set safely.")
                                                             End Sub)

    Dim thread2 As New System.Threading.Thread(threadParameters)
    thread2.Start()

End Sub

Private Sub WriteTextSafe(text As String)

    If (TextBox1.InvokeRequired) Then

        TextBox1.Invoke(Sub()
                            WriteTextSafe($"{text} (THREAD2)")
                        End Sub)

    Else
        TextBox1.Text = text
    End If

End Sub

例: BackgroundWorker を使用する

マルチスレッドを実装する簡単な方法として、イベントドリブン モデルが使用される System.ComponentModel.BackgroundWorker コンポーネントがあります。 バックグラウンド スレッドで、メイン スレッドとやりとりしない BackgroundWorker.DoWork イベントが発生します。 メイン スレッドで、BackgroundWorker.ProgressChanged および BackgroundWorker.RunWorkerCompleted イベント ハンドラーが実行され、メイン スレッドのコントロールを呼び出すことができます。

BackgroundWorker を使用してスレッドセーフな呼び出しを行う場合は、DoWork イベントを処理します。 バックグラウンド ワーカーが状態を報告するために使用するイベントには、ProgressChangedRunWorkerCompleted の 2 つがあります。 ProgressChanged イベントは、状態の更新をメイン スレッドに伝えるのに使用され、RunWorkerCompleted イベントはバックグラウンド ワーカーが作業の完了を伝えるために使用されます。 バックグラウンド スレッドを開始するには、BackgroundWorker.RunWorkerAsync を呼び出します。

この例では、DoWork イベントで 0 から 10 までカウントし、カウント間で 1 秒間一時停止します。 ProgressChanged イベント ハンドラーを使用して、数値がメイン スレッドに報告され、TextBox コントロールのText プロパティが設定されます。 ProgressChanged イベントが機能するには、BackgroundWorker.WorkerReportsProgress プロパティを true に設定する必要があります。

private void button1_Click(object sender, EventArgs e)
{
    if (!backgroundWorker1.IsBusy)
        backgroundWorker1.RunWorkerAsync();
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    int counter = 0;
    int max = 10;

    while (counter <= max)
    {
        backgroundWorker1.ReportProgress(0, counter.ToString());
        System.Threading.Thread.Sleep(1000);
        counter++;
    }
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) =>
    textBox1.Text = (string)e.UserState;
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    If (Not BackgroundWorker1.IsBusy) Then
        BackgroundWorker1.RunWorkerAsync()
    End If

End Sub

Private Sub BackgroundWorker1_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

    Dim counter = 0
    Dim max = 10

    While counter <= max

        BackgroundWorker1.ReportProgress(0, counter.ToString())
        System.Threading.Thread.Sleep(1000)

        counter += 1

    End While

End Sub

Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    TextBox1.Text = e.UserState
End Sub