Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Multithreading pode melhorar o desempenho de aplicativos do Windows Forms, mas o acesso aos controles do Windows Forms não é inerentemente seguro para threads. Multithreading pode expor seu código a bugs sérios e complexos. Dois ou mais threads ao manipular um controlo podem fazer com que este entre num estado inconsistente, resultando em condições de corrida, deadlocks e congelamentos. Se implementar multithreading na sua aplicação, certifique-se de chamar os controlos entre threads de forma segura. Para obter mais informações, consulte Managed threading best practices.
Há duas maneiras de chamar com segurança um controle Windows Forms de um thread que não criou esse controle. Use o método System.Windows.Forms.Control.Invoke para chamar um delegado criado no thread principal, que por sua vez chama o controle. Ou, implemente um System.ComponentModel.BackgroundWorker, que usa um modelo controlado por eventos para separar o trabalho feito no thread em segundo plano do relatório sobre os resultados.
Chamadas cruzadas 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 insegura para o controle System.Windows.Forms.TextBox. O manipulador de eventos Button1_Click cria um novo thread WriteTextUnsafe, que define diretamente a propriedade TextBox.Text do thread principal.
private void button2_Click(object sender, EventArgs e)
{
WriteTextUnsafe("Writing message #1 (UI THREAD)");
_ = Task.Run(() => WriteTextUnsafe("Writing message #2 (OTHER THREAD)"));
}
private void WriteTextUnsafe(string text) =>
textBox1.Text += $"{Environment.NewLine}{text}";
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
WriteTextUnsafe("Writing message #1 (UI THREAD)")
Task.Run(Sub() WriteTextUnsafe("Writing message #2 (OTHER THREAD)"))
End Sub
Private Sub WriteTextUnsafe(text As String)
TextBox1.Text += $"{Environment.NewLine}{text}"
End Sub
O depurador do Visual Studio deteta essas chamadas de thread inseguras gerando uma InvalidOperationException com a mensagem, operação Cross-thread não válida. Controle acessado a partir de um thread diferente do thread em que foi criado. O InvalidOperationException sempre ocorre para chamadas cruzadas inseguras durante a depuração do Visual Studio e pode ocorrer no tempo de execução do aplicativo. Você deve corrigir o problema, mas pode desativar a exceção definindo a propriedade Control.CheckForIllegalCrossThreadCalls como false.
Chamadas cruzadas seguras
Os aplicativos do Windows Forms seguem uma estrutura rígida semelhante a um contrato, semelhante a todas as outras estruturas da interface do usuário do Windows: todos os controles devem ser criados e acessados a partir do mesmo thread. Isso é importante porque o Windows requer que os aplicativos forneçam um único thread dedicado para entregar mensagens do sistema. Sempre que o Gerenciador de Janelas do Windows deteta uma interação com uma janela do aplicativo, como pressionar uma tecla, clicar do mouse ou redimensionar a janela, ele roteia essas informações para o thread que criou e gerencia a interface do usuário e as transforma em eventos acionáveis. Esse thread é conhecido como thread da interface do usuário.
Como o código executado em outro thread não pode acessar controles criados e gerenciados pelo thread da interface do usuário, o Windows Forms fornece maneiras de trabalhar com segurança com esses controles de outro thread, conforme demonstrado nos exemplos de código a seguir:
Exemplo: Use Control.InvokeAsync (.NET 9 e posterior)
O Control.InvokeAsync método (.NET 9+), que fornece marshaling assíncrono para o thread da interface do usuário.
Exemplo: Use o método Control.Invoke:
O método Control.Invoke, que chama um delegado a partir do thread principal para acionar o controle.
Exemplo: Usar um BackgroundWorker
Um componente BackgroundWorker, que oferece um modelo controlado por eventos.
Exemplo: Use Control.InvokeAsync (.NET 9 e posterior)
A partir do .NET 9, o Windows Forms inclui o InvokeAsync método, que fornece marshaling async-friendly para o thread da interface do usuário. Esse método é útil para manipuladores de eventos assíncronos e elimina muitos cenários comuns de deadlock.
Observação
Control.InvokeAsync só está disponível no .NET 9 e posterior. Ele não é suportado no .NET Framework.
Entendendo a diferença: Invoke vs InvokeAsync
Control.Invoke (Envio - Bloqueio):
- Envia o delegado de forma síncrona para a fila de mensagens do thread da interface do usuário.
- O thread de chamada aguarda até que o thread da interface do usuário processe o delegado.
- Pode levar a congelamentos da interface do usuário quando o delegado empacotado para a fila de mensagens está aguardando a chegada de uma mensagem (deadlock).
- Útil quando você tem resultados prontos para exibição no thread da interface do usuário, por exemplo: desativar um botão ou definir o texto de um controle.
Control.InvokeAsync (Lançamento - Sem bloqueio):
- Publica de forma assíncrona o delegado na fila de mensagens do thread da interface do usuário, em vez de aguardar a conclusão da invocação.
- Retorna imediatamente sem bloquear o thread de chamada.
- Devolve um
Taskque pode ser aguardado para conclusão. - Ideal para cenários assíncronos e evita gargalos de thread da interface do usuário.
Vantagens do InvokeAsync
Control.InvokeAsync tem várias vantagens sobre o método mais antigo Control.Invoke . Ele retorna um Task que você pode esperar, fazendo com que funcione bem com async e await code. Ele também evita problemas comuns de deadlock que podem acontecer ao misturar código assíncrono com chamadas de chamada síncronas. Ao contrário Control.Invokedo , o método não bloqueia o thread de chamada, o InvokeAsync que mantém seus aplicativos responsivos.
O método suporta cancelamento através do CancellationToken, para que você possa cancelar operações quando necessário. Ele também lida com exceções corretamente, passando-as de volta para o seu código para que você possa lidar com erros adequadamente. O .NET 9 inclui avisos do compilador (WFO2001) que ajudam você a usar o método corretamente.
Para obter orientações abrangentes sobre manipuladores de eventos assíncronos e práticas recomendadas, consulte Visão geral de eventos.
Escolhendo a sobrecarga InvokeAsync correta
Control.InvokeAsync Fornece quatro sobrecargas para diferentes cenários:
| Sobrecarga | Caso de uso | Example |
|---|---|---|
InvokeAsync(Action) |
Operação de sincronização, sem valor de retorno. | Atualizar propriedades de controle. |
InvokeAsync<T>(Func<T>) |
Operação de sincronização, com valor de retorno. | Obtenha o estado de controle. |
InvokeAsync(Func<CancellationToken, ValueTask>) |
Operação assíncrona, sem valor de retorno.* | Atualizações de interface do usuário de longa duração. |
InvokeAsync<T>(Func<CancellationToken, ValueTask<T>>) |
Operação assíncrona, com valor de retorno.* | Busca de dados assíncrona com resultado. |
*O Visual Basic não suporta a espera de um ValueTaskarquivo .
O exemplo a seguir demonstra o uso InvokeAsync para atualizar controles com segurança de um thread em segundo plano:
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// Perform background work
await Task.Run(async () =>
{
for (int i = 0; i <= 100; i += 10)
{
// Simulate work
await Task.Delay(100);
// Create local variable to avoid closure issues
int currentProgress = i;
// Update UI safely from background thread
await loggingTextBox.InvokeAsync(() =>
{
loggingTextBox.Text = $"Progress: {currentProgress}%";
});
}
});
loggingTextBox.Text = "Operation completed!";
}
finally
{
button1.Enabled = true;
}
}
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles button1.Click
button1.Enabled = False
Try
' Perform background work
Await Task.Run(Async Function()
For i As Integer = 0 To 100 Step 10
' Simulate work
Await Task.Delay(100)
' Create local variable to avoid closure issues
Dim currentProgress As Integer = i
' Update UI safely from background thread
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = $"Progress: {currentProgress}%"
End Sub)
Next
End Function)
' Update UI after completion
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = "Operation completed!"
End Sub)
Finally
button1.Enabled = True
End Try
End Sub
Para operações assíncronas que precisam ser executadas no thread da interface do usuário, use a sobrecarga assíncrona:
private async void button2_Click(object sender, EventArgs e)
{
button2.Enabled = false;
try
{
loggingTextBox.Text = "Starting operation...";
// Dispatch and run on a new thread, but wait for tasks to finish
// Exceptions are rethrown here, because await is used
await Task.WhenAll(Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync));
// Dispatch and run on a new thread, but don't wait for task to finish
// Exceptions are not rethrown here, because await is not used
_ = Task.Run(SomeApiCallAsync);
}
catch (OperationCanceledException)
{
loggingTextBox.Text += "Operation canceled.";
}
catch (Exception ex)
{
loggingTextBox.Text += $"Error: {ex.Message}";
}
finally
{
button2.Enabled = true;
}
}
private async Task SomeApiCallAsync()
{
using var client = new HttpClient();
// Simulate random network delay
await Task.Delay(Random.Shared.Next(500, 2500));
// Do I/O asynchronously
string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");
// Marshal back to UI thread
await this.InvokeAsync(async (cancelToken) =>
{
loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}";
});
// Do more async I/O ...
}
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles button2.Click
button2.Enabled = False
Try
loggingTextBox.Text = "Starting operation..."
' Dispatch and run on a new thread, but wait for tasks to finish
' Exceptions are rethrown here, because await is used
Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync))
' Dispatch and run on a new thread, but don't wait for task to finish
' Exceptions are not rethrown here, because await is not used
Call Task.Run(AddressOf SomeApiCallAsync)
Catch ex As OperationCanceledException
loggingTextBox.Text += "Operation canceled."
Catch ex As Exception
loggingTextBox.Text += $"Error: {ex.Message}"
Finally
button2.Enabled = True
End Try
End Sub
Private Async Function SomeApiCallAsync() As Task
Using client As New HttpClient()
' Simulate random network delay
Await Task.Delay(Random.Shared.Next(500, 2500))
' Do I/O asynchronously
Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")
' Marshal back to UI thread
' Extra work here in VB to handle ValueTask conversion
Await Me.InvokeAsync(DirectCast(
Async Function(cancelToken As CancellationToken) As Task
loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"
End Function,
Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task
)
' Do more Async I/O ...
End Using
End Function
Observação
Se você estiver usando o Visual Basic, o trecho de código anterior usou um método de extensão para converter um ValueTask em um Taskarquivo . O código do método de extensão está disponível no GitHub.
Exemplo: Use o método Control.Invoke
O exemplo a seguir demonstra um padrão para garantir chamadas thread-safe para um controle Windows Forms. Ele consulta a propriedade System.Windows.Forms.Control.InvokeRequired, que compara o ID de thread de criação do controle com o ID de thread de chamada. Se forem diferentes, você deve chamar o método Control.Invoke.
O WriteTextSafe permite definir a propriedade TextBox do controle Text para um novo valor. O método consulta InvokeRequired. Se InvokeRequired retornar true, WriteTextSafe chama-se recursivamente, passando o método como delegado para o método Invoke. 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.
private void button1_Click(object sender, EventArgs e)
{
WriteTextSafe("Writing message #1");
_ = Task.Run(() => WriteTextSafe("Writing message #2"));
}
public void WriteTextSafe(string text)
{
if (textBox1.InvokeRequired)
textBox1.Invoke(() => WriteTextSafe($"{text} (NON-UI THREAD)"));
else
textBox1.Text += $"{Environment.NewLine}{text}";
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
WriteTextSafe("Writing message #1")
Task.Run(Sub() WriteTextSafe("Writing message #2"))
End Sub
Private Sub WriteTextSafe(text As String)
If (TextBox1.InvokeRequired) Then
TextBox1.Invoke(Sub()
WriteTextSafe($"{text} (NON-UI THREAD)")
End Sub)
Else
TextBox1.Text += $"{Environment.NewLine}{text}"
End If
End Sub
Para obter mais informações sobre como difere Invoke do InvokeAsync, consulte Compreendendo a diferença: Invoke vs InvokeAsync.
Exemplo: Usar um BackgroundWorker
Uma maneira fácil de implementar cenários multi-threading e, ao mesmo tempo, garantir que o acesso a um controle ou formulário seja executado apenas no thread principal (thread da interface do usuário), é com o System.ComponentModel.BackgroundWorker componente, que usa um modelo controlado por eventos. O thread de plano de fundo gera 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.
Importante
O BackgroundWorker componente não é mais a abordagem recomendada para cenários assíncronos em aplicativos Windows Forms. Embora continuemos oferecendo suporte a esse componente para compatibilidade com versões anteriores, ele aborda apenas o descarregamento da carga de trabalho do processador do thread da interface do usuário para outro thread. Ele não lida com outros cenários assíncronos, como E/S de arquivos ou operações de rede em que o processador pode não estar trabalhando ativamente.
Para programação assíncrona moderna, use async métodos com await . Se você precisar descarregar explicitamente o trabalho intensivo do processador, use Task.Run para criar e iniciar uma nova tarefa, que você pode aguardar como qualquer outra operação assíncrona. Para obter mais informações, consulte Exemplo: Usar Control.InvokeAsync (.NET 9 e posterior) e Operações e eventos entre threads.
Para fazer uma chamada thread-safe usando BackgroundWorker, manipule o evento DoWork. Há dois eventos que o trabalhador em segundo plano usa para relatar o status: ProgressChanged e RunWorkerCompleted. O ProgressChanged evento é usado para comunicar atualizações de status para o thread principal e o RunWorkerCompleted evento é usado para sinalizar que o trabalhador em segundo plano foi concluído. Para iniciar o thread em segundo plano, chame BackgroundWorker.RunWorkerAsync.
O exemplo conta de 0 a 10 no evento DoWork, pausando por um segundo entre as contagens. Usa o manipulador de eventos ProgressChanged para retornar o número ao thread principal e ajustar a propriedade TextBox do controle Text. Para que o evento ProgressChanged funcione, a propriedade BackgroundWorker.WorkerReportsProgress deve ser definida como true.
private void button1_Click(object sender, EventArgs e)
{
if (!backgroundWorker1.IsBusy)
backgroundWorker1.RunWorkerAsync(); // Not awaitable
}
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() ' Not awaitable
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