マルチスレッドは 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)
{
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 つの方法を示しています。
- System.Windows.Forms.Control.Invoke メソッド。メイン スレッドからデリゲートを呼び出してコントロールを呼び出します。
- イベント ドリブン モデルを提供する System.ComponentModel.BackgroundWorker コンポーネント。
どちらの例でも、バックグラウンド スレッドは 1 秒間スリープ状態にして、そのスレッドで実行されている作業をシミュレートします。
例: Invoke メソッドを使用する
次の例は、Windows フォーム コントロールへのスレッド セーフな呼び出しを保証するパターンを示しています。 System.Windows.Forms.Control.InvokeRequired プロパティに対してクエリを実行します。このプロパティは、コントロールの作成スレッド ID と呼び出し元のスレッド ID を比較します。 異なる場合は、 Control.Invoke メソッドを呼び出す必要があります。
WriteTextSafe
を使用すると、TextBox コントロールの Text プロパティを新しい値に設定できます。 メソッドは InvokeRequiredクエリを実行します。
InvokeRequiredがtrue
を返す場合は、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 イベントを処理します。 バックグラウンド ワーカーが状態を報告するために使用するイベントには、 ProgressChanged と RunWorkerCompletedの 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
.NET Desktop feedback