Procedura: Effettuare chiamate thread-safe ai controlli Windows Forms
Il multithreading può migliorare le prestazioni delle app Windows Forms, ma l'accesso ai Windows Forms non è intrinsecamente thread-safe. Il multithreading può esporre il codice a bug molto gravi e complessi. Due o più thread che modificano un controllo possono forzare il controllo in uno stato incoerente e causare race conditions, deadlock e blocchi o blocchi. Se si implementa il multithreading nell'app, assicurarsi di chiamare i controlli tra thread in modo thread-safe. Per altre informazioni, vedere Procedure consigliate per il threading gestito.
Esistono due modi per chiamare in modo sicuro Windows Forms controllo da un thread che non ha creato tale controllo. È possibile usare il System.Windows.Forms.Control.Invoke metodo per chiamare un delegato creato nel thread principale, che a sua volta chiama il controllo . In caso contrario, è possibile System.ComponentModel.BackgroundWorkerimplementare un oggetto , che usa un modello basato su eventi per separare le operazioni eseguite nel thread in background dalla creazione di report sui risultati.
Chiamate cross-thread non sicure
Non è sicuro chiamare un controllo direttamente da un thread che non lo ha creato. Nel frammento di codice seguente viene illustrata una chiamata non sicura al System.Windows.Forms.TextBox controllo . Il Button1_Click
gestore eventi crea un nuovo WriteTextUnsafe
thread, che imposta direttamente la proprietà del TextBox.Text thread principale.
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
Il debugger Visual Studio rileva queste chiamate di thread non sicure generando un'operazione tra thread InvalidOperationException non valida con il messaggio . Controllo "" a cui si accede da un thread diverso da quello in cui è stato creato. Si InvalidOperationException verifica sempre per le chiamate cross-thread non sicure durante Visual Studio debug e può verificarsi in fase di esecuzione dell'app. È consigliabile risolvere il problema, ma è possibile disabilitare l'eccezione impostando la Control.CheckForIllegalCrossThreadCalls proprietà su false
.
Cassaforte chiamate tra thread
Gli esempi di codice seguenti illustrano due modi per chiamare in modo sicuro un controllo Windows Forms da un thread che non lo ha creato:
- Il System.Windows.Forms.Control.Invoke metodo , che chiama un delegato dal thread principale per chiamare il controllo.
- Componente System.ComponentModel.BackgroundWorker che offre un modello basato su eventi.
In entrambi gli esempi, il thread in background viene in sospensione per un secondo per simulare il lavoro eseguito in tale thread.
È possibile compilare ed eseguire questi esempi .NET Framework app dalla riga di comando C# o Visual Basic comando. Per altre informazioni, vedere Compilazione da riga di comando con csc.exe o Compilazione dalla riga di comando (Visual Basic).
A partire da .NET Core 3.0, è anche possibile compilare ed eseguire gli esempi come app .NET Core Windows da una cartella che contiene un file di progetto .NET Core Windows Forms <>nome cartella.csproj.
Esempio: Usare il metodo Invoke con un delegato
Nell'esempio seguente viene illustrato un modello per garantire chiamate thread-safe a un Windows Forms controllo . Esegue una query sulla System.Windows.Forms.Control.InvokeRequired proprietà , che confronta l'ID thread di creazione del controllo con l'ID del thread chiamante. Se gli ID thread sono gli stessi, chiama direttamente il controllo . Se gli ID di thread sono diversi, Control.Invoke chiama il metodo con un delegato dal thread principale, che effettua la chiamata effettiva al controllo.
abilita SafeCallDelegate
l'impostazione TextBox della proprietà del Text controllo. Il metodo WriteTextSafe
esegue una query su InvokeRequired. Se InvokeRequired restituisce true
, WriteTextSafe
passa al SafeCallDelegate
metodo Invoke per effettuare la chiamata effettiva al controllo. Se InvokeRequired restituisce false
, WriteTextSafe
imposta direttamente TextBox.Text . Il Button1_Click
gestore eventi crea il nuovo thread ed esegue il WriteTextSafe
metodo .
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
Esempio: Usare un gestore eventi BackgroundWorker
Un modo semplice per implementare il multithreading è con il System.ComponentModel.BackgroundWorker componente , che usa un modello basato su eventi. Il thread in background esegue l'evento BackgroundWorker.DoWork , che non interagisce con il thread principale. Il thread principale esegue i BackgroundWorker.ProgressChanged gestori BackgroundWorker.RunWorkerCompleted eventi e , che possono chiamare i controlli del thread principale.
Per effettuare una chiamata thread-safe usando BackgroundWorker, creare un metodo nel thread in background per eseguire le operazioni e associarlo all'evento DoWork . Creare un altro metodo nel thread principale per segnalare i risultati del lavoro in background e associarlo all'evento ProgressChanged o RunWorkerCompleted . Per avviare il thread in background, chiamare BackgroundWorker.RunWorkerAsync.
Nell'esempio viene utilizzato RunWorkerCompleted il gestore eventi per impostare la TextBox proprietà del Text controllo . Per un esempio d'uso dell'evento ProgressChanged , vedere 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