Como fazer chamadas thread-safe para controles do Windows Forms
O multithreading pode melhorar o desempenho de aplicativos do Windows Forms, mas o acesso aos controles do Windows Forms não é inerentemente thread-safe. O multithreading pode expor seu código a bugs muito sérios e complexos. Dois ou mais segmentos de execução (threads) manipulando um controle podem forçar o controle a um estado de inconsistência e levar a condições de concorrência, deadlocks e congelamentos ou travamentos. Se você implementar o multithreading em seu aplicativo, certifique-se de chamar controles entre threads de forma segura. 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 System.Windows.Forms.Control.Invoke para chamar um delegado criado no thread principal, que, por sua vez, chama o controle. Ou você pode implementar um System.ComponentModel.BackgroundWorker, que usa um modelo orientado a eventos para separar o trabalho feito na thread de segundo plano da elaboração de relatórios sobre os resultados.
Chamadas entre threads não seguras
Não é seguro chamar um controle diretamente de um thread que não o criou. O snippet de código a seguir ilustra uma chamada não segura para o controle System.Windows.Forms.TextBox. O manipulador de eventos Button1_Click
cria um novo thread de WriteTextUnsafe
, que define diretamente a propriedade TextBox.Text do thread principal.
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 entre threads não segura. Controle "" acessado por um thread diferente daquele em que foi criado. O InvalidOperationException sempre ocorre para operações entre threads 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 propriedade Control.CheckForIllegalCrossThreadCalls como false
.
Chamadas seguras entre subprocessos
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:
- O método System.Windows.Forms.Control.Invoke, que chama um delegado da thread principal para invocar o controle.
- Um componente System.ComponentModel.BackgroundWorker, que oferece um modelo controlado por eventos.
Em ambos os exemplos, o thread de plano de fundo fica inativo 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 na linha de comando C# ou Visual Basic. Para obter mais informações, consulte compilação de linha de comando com csc.exe ou Compilação 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 de uma pasta que tem um nome de pasta do Windows Forms <do .NET Core>arquivo de projeto .csproj.
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 do Windows Forms. Ele consulta a propriedade System.Windows.Forms.Control.InvokeRequired, que compara o ID do thread que criou o controle com o ID do thread de chamada. Se os IDs de thread forem iguais, chama o controle diretamente. Se as IDs de thread forem diferentes, ela chamará o método Control.Invoke com um delegado do thread principal, que faz a chamada real para o controle.
O SafeCallDelegate
permite definir a propriedade Text do controle TextBox. O método WriteTextSafe
consulta InvokeRequired. Se InvokeRequired retornar true
, WriteTextSafe
passa o SafeCallDelegate
para o método Invoke para realizar a chamada efetiva ao controle. Se InvokeRequired retornar false
, WriteTextSafe
definirá o TextBox.Text diretamente. O manipulador de eventos Button1_Click
cria o novo thread e executa o método WriteTextSafe
.
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 o multithreading é com o componente System.ComponentModel.BackgroundWorker, que usa um modelo controlado por eventos. O thread em segundo plano executa o evento BackgroundWorker.DoWork, que não interage com o thread principal. O thread principal executa os manipuladores de eventos BackgroundWorker.ProgressChanged e BackgroundWorker.RunWorkerCompleted, que podem chamar os 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 associe-o ao evento DoWork. Crie outro método no thread principal para relatar os resultados do trabalho em segundo plano e associá-lo ao evento ProgressChanged ou RunWorkerCompleted. Para iniciar o thread em segundo plano, chame BackgroundWorker.RunWorkerAsync.
O exemplo usa o manipulador de eventos RunWorkerCompleted para definir a propriedade Text do controle TextBox. Para obter um exemplo usando o evento ProgressChanged, 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
Consulte também
.NET Desktop feedback