Como: fazer chamadas thread-safe para controles do Windows Forms

O multithreading pode melhorar o desempenho dos aplicativos do Windows Forms, mas o acesso aos controles do Windows Forms não é inerentemente seguro para threads. O multithreading pode expor seu código a bugs muito sérios e complexos. Dois ou mais threads manipulando um controle podem forçar o controle a um estado inconsistente e levar a condições de corrida, bloqueios e congelamentos ou travamentos. Se você implementar multithreading em seu aplicativo, certifique-se de chamar controles cross-thread de uma maneira thread-safe. Para obter mais informações, consulte Práticas recomendadas de threading gerenciado.

Há duas maneiras de chamar com segurança um controle do Windows Forms de um thread que não criou esse controle. Você pode usar o método para chamar um delegado criado no thread principal, que por sua vez chama o System.Windows.Forms.Control.Invoke controle. Ou, você pode implementar um , que usa um System.ComponentModel.BackgroundWorkermodelo controlado por eventos para separar o trabalho feito no thread em segundo plano do relatório sobre os resultados.

Chamadas de thread cruzado inseguras

Não é seguro chamar um controle diretamente de um thread que não o criou. O trecho de código a seguir ilustra uma chamada não segura para o System.Windows.Forms.TextBox controle. O Button1_Click manipulador de eventos cria um novo WriteTextUnsafe thread, que define a propriedade do TextBox.Text thread principal diretamente.

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

O depurador do Visual Studio detecta essas chamadas de thread não seguras gerando uma InvalidOperationException com a mensagem, operação de thread cruzado não válida. Controle "" acessado a partir de um thread diferente do thread em que foi criado. O InvalidOperationException sempre ocorre para chamadas de thread cruzado não seguras durante a depuração do Visual Studio e pode ocorrer no tempo de execução do aplicativo. Você deve corrigir o problema, mas pode desabilitar a exceção definindo a Control.CheckForIllegalCrossThreadCalls propriedade como false.

Chamadas cross-thread seguras

Os exemplos de código a seguir demonstram duas maneiras de chamar com segurança um controle do Windows Forms de um thread que não o criou:

  1. O System.Windows.Forms.Control.Invoke método, que chama um delegado do thread principal para chamar o controle.
  2. Um System.ComponentModel.BackgroundWorker componente, que oferece um modelo orientado a eventos.

Em ambos os exemplos, o thread em segundo plano fica em repouso por um segundo para simular o trabalho que está sendo feito nesse thread.

Você pode criar e executar esses exemplos como aplicativos do .NET Framework a partir da linha de comando C# ou Visual Basic. Para obter mais informações, consulte Compilação de linha de comando com csc.exe ou Compilar a partir da linha de comando (Visual Basic).

A partir do .NET Core 3.0, você também pode criar e executar os exemplos como aplicativos do Windows .NET Core a partir de uma pasta que tenha um arquivo de projeto .>csproj de nome da pasta do Windows Forms <do .NET Core.

Exemplo: Usar o método Invoke com um delegado

O exemplo a seguir demonstra um padrão para garantir chamadas thread-safe para um controle Windows Forms. Ele consulta a propriedade, que compara a ID de thread de criação do controle com a System.Windows.Forms.Control.InvokeRequired ID de thread de chamada. Se os IDs de thread forem os mesmos, ele chamará o controle diretamente. Se os IDs de thread forem diferentes, ele chamará o método com um delegado do thread principal, o que fará a chamada real para o Control.Invoke controle.

O SafeCallDelegate habilita definindo a TextBox propriedade do Text controle. O WriteTextSafe método consulta InvokeRequired. Se InvokeRequired retorna true, WriteTextSafe passa o para o método para fazer a chamada real para o SafeCallDelegateInvoke controle. Se InvokeRequired retornar false, WriteTextSafe define o TextBox.Text diretamente. O Button1_Click manipulador de eventos cria o novo thread e executa o WriteTextSafe método.

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

Exemplo: Usar um manipulador de eventos BackgroundWorker

Uma maneira fácil de implementar multithreading é com o System.ComponentModel.BackgroundWorker componente, que usa um modelo orientado a eventos. O thread em segundo plano executa o evento, que não interage com o BackgroundWorker.DoWork thread principal. O thread principal executa os manipuladores de eventos e BackgroundWorker.RunWorkerCompleted , que podem chamar os BackgroundWorker.ProgressChanged controles do thread principal.

Para fazer uma chamada thread-safe usando BackgroundWorker, crie um método no thread em segundo plano para fazer o trabalho e vincule-o DoWork ao evento. Crie outro método no thread principal para relatar os resultados do trabalho em segundo plano e vinculá-lo ao ProgressChanged evento or RunWorkerCompleted . Para iniciar o thread em segundo plano, chame BackgroundWorker.RunWorkerAsync.

O exemplo usa o RunWorkerCompleted manipulador de eventos para definir a TextBox propriedade do Text controle. Para obter um exemplo usando o ProgressChanged evento, consulte 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

Confira também