方法: 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 つの方法を示しています。
- System.Windows.Forms.Control.Invoke メソッド。コントロールを呼び出すメイン スレッドからデリゲートが呼び出されます。
- 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
により、SafeCallDelegate
が Invoke メソッドに渡され、コントロールへの実際の呼び出しが行われます。 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
関連項目
.NET Desktop feedback
フィードバック
フィードバックの送信と表示