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

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

コントロールを作成していないスレッドから 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)
{
    thread2 = new Thread(new ThreadStart(WriteTextUnsafe));
    thread2.Start();
}
private void WriteTextUnsafe()
{
    textBox1.Text = "This text was set unsafely.";
}
Private Sub Button1_Click(ByVal sender As Object, e As EventArgs) Handles Button1.Click
    Thread2 = New Thread(New ThreadStart(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 秒間スリープして、そのスレッドで実行されている作業をシミュレートします。

これらの例は、C# または Visual Basic コマンド ラインから .NET Framework アプリとしてビルドし、実行できます。 詳細については、csc.exe を使用したコマンド ラインからのビルドに関するページ、または「コマンド ラインからのビルド (Visual Basic)」を参照してください。

.NET Core 3.0 以降では、.NET Core Windows フォームの <folder name>.csproj プロジェクト ファイルが含まれるフォルダーから、Windows .NET Core アプリとして例を構築し、実行することもできます。

例: デリゲートで Invoke メソッドを使用する

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

SafeCallDelegate を使用すると、TextBox コントロールの Text プロパティを設定できます。 WriteTextSafe メソッドにより、InvokeRequired のクエリが行われます。 InvokeRequired から true が返される場合、WriteTextSafe により、SafeCallDelegateInvoke メソッドに渡され、コントロールへの実際の呼び出しが行われます。 InvokeRequired から false が返される場合、WriteTextSafe により、TextBox.Text が直接設定されます。 Button1_Click イベント ハンドラーにより、新しいスレッドが作成され、WriteTextSafe メソッドが実行されます。

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class InvokeThreadSafeForm : Form
{
    private delegate void SafeCallDelegate(string text);
    private Button button1;
    private TextBox textBox1;
    private Thread thread2 = null;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new InvokeThreadSafeForm());
    }
    public InvokeThreadSafeForm()
    {
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        thread2 = new Thread(new ThreadStart(SetText));
        thread2.Start();
        Thread.Sleep(1000);
    }

    private void WriteTextSafe(string text)
    {
        if (textBox1.InvokeRequired)
        {
            var d = new SafeCallDelegate(WriteTextSafe);
            textBox1.Invoke(d, new object[] { text });
        }
        else
        {
            textBox1.Text = text;
        }
    }

    private void SetText()
    {
        WriteTextSafe("This text was set safely.");
    }
}
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class InvokeThreadSafeForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New InvokeThreadSafeForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox
    Dim Thread2 as Thread = Nothing

    Delegate Sub SafeCallDelegate(text As String)

    Private Sub New()
        Button1 = New Button()
        With Button1
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
            .Text = "Set text safely"
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Thread2 = New Thread(New ThreadStart(AddressOf SetText))
        Thread2.Start()
        Thread.Sleep(1000)
    End Sub

    Private Sub WriteTextSafe(text As String)
        If TextBox1.InvokeRequired Then
            Dim d As New SafeCallDelegate(AddressOf SetText)
            TextBox1.Invoke(d, New Object() {text})
        Else
            TextBox1.Text = text
        End If
    End Sub

    Private Sub SetText()
        WriteTextSafe("This text was set safely.")
    End Sub
End Class

例: BackgroundWorker イベント ハンドラーを使用する

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

BackgroundWorker を使用してスレッドセーフな呼び出しを行うには、作業を行うメソッドをバックグラウンド スレッドに作成して、DoWork イベントにバインドします。 バックグラウンド作業の結果を報告するもう 1 つのメソッドをメイン スレッドに作成し、ProgressChanged または RunWorkerCompleted イベントにバインドします。 バックグラウンド スレッドを開始するには、BackgroundWorker.RunWorkerAsync を呼び出します。

この例では、RunWorkerCompleted イベント ハンドラーを使用して、TextBox コントロールの Text プロパティを設定します。 ProgressChanged イベントの使用例については、BackgroundWorker に関する記事をご覧ください。

using System;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class BackgroundWorkerForm : Form
{
    private BackgroundWorker backgroundWorker1;
    private Button button1;
    private TextBox textBox1;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new BackgroundWorkerForm());
    }
    public BackgroundWorkerForm()
    {
        backgroundWorker1 = new BackgroundWorker();
        backgroundWorker1.DoWork += new DoWorkEventHandler(BackgroundWorker1_DoWork);
        backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely with BackgroundWorker"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }
    private void Button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    private void BackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        // Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000);
        e.Result = "This text was set safely by BackgroundWorker.";
    }

    private void BackgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        textBox1.Text = e.Result.ToString();
    }
}
Imports System.ComponentModel
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class BackgroundWorkerForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New BackgroundWorkerForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents BackgroundWorker1 As BackgroundWorker
    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox

    Private Sub New()
        BackgroundWorker1 = New BackgroundWorker()
        Button1 = New Button()
        With Button1
            .Text = "Set text safely with BackgroundWorker"
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) _
     Handles BackgroundWorker1.DoWork
        ' Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000)
        e.Result = "This text was set safely by BackgroundWorker."
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) _
     Handles BackgroundWorker1.RunWorkerCompleted
        textBox1.Text = e.Result.ToString()
    End Sub
End Class

関連項目