Condividi tramite


Come effettuare chiamate thread-safe ai controlli

Il multithreading può migliorare le prestazioni delle applicazioni Windows Forms, ma l'accesso ai controlli di Windows Forms 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 condizioni di gara, stalli e blocchi o sospensioni. 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 metodo System.Windows.Forms.Control.Invoke per chiamare un delegato creato nel thread principale, che a sua volta invoca il controllo. In alternativa, implementare un System.ComponentModel.BackgroundWorker, che usa un modello basato su eventi per separare le operazioni eseguite nel thread in background dalla segnalazione dei 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 controllo System.Windows.Forms.TextBox. Il gestore eventi Button1_Click crea un nuovo thread WriteTextUnsafe, che imposta direttamente la proprietà TextBox.Text del 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 sicure generando un InvalidOperationException con il messaggio, operazione tra thread non valida. Controllo accesso da un thread diverso da quello su cui è stato creato. Il InvalidOperationException si verifica sempre per le chiamate di thread insicure durante il debug in Visual Studio e potrebbe verificarsi durante l'esecuzione dell'app. È consigliabile risolvere il problema, ma è possibile disabilitare l'eccezione impostando la proprietà Control.CheckForIllegalCrossThreadCalls su false.

Chiamate sicure tra thread diversi

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:

  1. Metodo System.Windows.Forms.Control.Invoke, che chiama un delegato dall'interno del thread principale per invocare il controllo.
  2. 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 la sicurezza delle chiamate thread-safe a un controllo di Windows Forms. Interroga la proprietà System.Windows.Forms.Control.InvokeRequired, che confronta l'ID del thread di creazione del controllo con l'ID del thread chiamante. Se sono diversi, dovresti chiamare il metodo Control.Invoke.

Il WriteTextSafe consente di impostare la proprietà TextBox del controllo Text su un nuovo valore. Il metodo interroga InvokeRequired. Se InvokeRequired restituisce true, WriteTextSafe chiama in modo ricorsivo se stesso, passando il metodo come delegato al metodo Invoke. Se InvokeRequired restituisce false, WriteTextSafe imposta direttamente il TextBox.Text. Il gestore eventi Button1_Click crea il nuovo thread ed esegue il metodo 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

Esempio: Usare un BackgroundWorker

Un modo semplice per implementare il multithreading consiste nel componente System.ComponentModel.BackgroundWorker, 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 gestori di eventi BackgroundWorker.ProgressChanged e BackgroundWorker.RunWorkerCompleted, che possono invocare i controlli del thread principale.

Per effettuare una chiamata thread-safe usando BackgroundWorker, gestire l'evento DoWork. Esistono due eventi usati dal processo 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 worker in background ha completato il 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 gestore eventi ProgressChanged per segnalare il numero al thread principale e impostare la proprietà TextBox del controllo Text. Affinché l'evento ProgressChanged funzioni, la proprietà BackgroundWorker.WorkerReportsProgress 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