Come effettuare chiamate thread-safe ai controlli (Windows Form .NET)
Il multithreading può migliorare le prestazioni delle app Windows Form, ma l'accesso ai controlli Windows Form non è intrinsecamente thread-safe. Il multithreading può esporre il codice a bug gravi e complessi. Due o più thread che manipolano un controllo possono forzare il controllo in uno stato incoerente e causare race condition, 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 un controllo Windows Form da un thread che non ha creato tale controllo. Usare il System.Windows.Forms.Control.Invoke metodo per chiamare un delegato creato nel thread principale, che a sua volta chiama il controllo. In alternativa, implementare un oggetto , che usa un System.ComponentModel.BackgroundWorkermodello basato su eventi per separare le operazioni eseguite nel thread in background dalla creazione di report sui risultati.
Chiamate tra thread non sicure
Non è sicuro chiamare un controllo direttamente da un thread che non lo ha creato. Il frammento di codice seguente illustra 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)
{
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
Il debugger di Visual Studio rileva queste chiamate di thread non sicuri generando un oggetto InvalidOperationException con il messaggio Operazione tra thread non valida. Controllare l'accesso da un thread diverso dal thread in cui è stato creato. Si InvalidOperationException verifica sempre per le chiamate tra thread non sicuri durante il debug di Visual Studio e possono verificarsi in fase di esecuzione dell'app. È consigliabile risolvere il problema, ma è possibile disabilitare l'eccezione impostando la Control.CheckForIllegalCrossThreadCalls proprietà su false
.
Chiamate tra thread sicuri
Gli esempi di codice seguenti illustrano due modi per chiamare in modo sicuro un controllo Windows Form da un thread che non lo ha creato:
- Metodo System.Windows.Forms.Control.Invoke 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 rimane inattivo per un secondo per simulare il lavoro eseguito in tale thread.
Esempio: usare il metodo Invoke
Nell'esempio seguente viene illustrato un modello per garantire chiamate thread-safe a un controllo Windows Form. Esegue una query sulla System.Windows.Forms.Control.InvokeRequired proprietà , che confronta l'ID thread del controllo con l'ID thread chiamante. Se sono diversi, è necessario chiamare il Control.Invoke metodo .
WriteTextSafe
Abilita l'impostazione della TextBox proprietà del Text controllo su un nuovo valore. Il metodo esegue una query su InvokeRequired. Se InvokeRequired restituisce true
, WriteTextSafe
chiama in modo ricorsivo se stesso, passando il metodo come delegato al Invoke metodo . Se InvokeRequired restituisce false
, WriteTextSafe
imposta direttamente .TextBox.Text Il Button1_Click
gestore eventi crea il nuovo thread ed esegue il WriteTextSafe
metodo .
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
Esempio: Usare un backgroundWorker
Un modo semplice per implementare il multithreading consiste nel System.ComponentModel.BackgroundWorker componente , che usa un modello basato su eventi. Il thread in background genera l'evento BackgroundWorker.DoWork , che non interagisce con il thread principale. Il thread principale esegue i BackgroundWorker.ProgressChanged gestori eventi e BackgroundWorker.RunWorkerCompleted , che possono chiamare i controlli del thread principale.
Per effettuare una chiamata thread-safe tramite BackgroundWorker, gestire l'evento DoWork . Esistono due eventi usati dal ruolo di lavoro in background per segnalare lo stato: ProgressChanged e RunWorkerCompleted. L'evento ProgressChanged
viene usato per comunicare gli aggiornamenti dello stato al thread principale e l'evento RunWorkerCompleted
viene usato per segnalare che il ruolo di lavoro in background ha completato il proprio lavoro. Per avviare il thread in background, chiamare BackgroundWorker.RunWorkerAsync.
L'esempio conta da 0 a 10 nell'evento DoWork
, sospendo un secondo tra i conteggi. Usa il ProgressChanged gestore eventi per segnalare il numero al thread principale e impostare la TextBox proprietà del Text controllo. Affinché l'evento ProgressChanged funzioni, la BackgroundWorker.WorkerReportsProgress proprietà deve essere impostata su 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