Procedura: Effettuare chiamate thread-safe ai controlli Windows Form

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 molto 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. È 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 alternativa, è possibile implementare un oggetto , che usa un System.ComponentModel.BackgroundWorkermodello basato su eventi per separare il lavoro svolto 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)
{
    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 di Visual Studio rileva queste chiamate di thread non sicuri generando un oggetto InvalidOperationException con il messaggio Operazione tra thread non valida. Controllare "" a cui è stato eseguito 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.

Cassaforte chiamate tra thread

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 dal thread principale per chiamare 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.

È possibile compilare ed eseguire questi esempi come app .NET Framework dalla riga di comando di C# o Visual Basic. Per altre informazioni, vedere Compilazione da riga di comando con csc.exe o Build dalla riga di comando (Visual Basic).For more information, see Command-line building with csc.exe or Build from the command line (Visual Basic).

A partire da .NET Core 3.0, è anche possibile compilare ed eseguire gli esempi come app di Windows .NET Core da una cartella con un file di progetto .NET Core Windows Form <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 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 gli ID del thread sono gli stessi, chiama direttamente il controllo . Se gli ID del thread sono diversi, chiama il Control.Invoke metodo con un delegato dal thread principale, che effettua la chiamata effettiva al controllo.

SafeCallDelegate Abilita l'impostazione della TextBox proprietà del Text controllo. Il WriteTextSafe metodo esegue una query su InvokeRequired. Se InvokeRequired restituisce true, WriteTextSafe passa l'oggetto SafeCallDelegate al Invoke metodo 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 consiste nel 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 eventi e BackgroundWorker.RunWorkerCompleted , 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 il lavoro 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 il RunWorkerCompleted gestore eventi per impostare la TextBox proprietà del Text controllo. Per un esempio relativo all'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

Vedi anche