Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
O multithreading pode aprimorar 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 sérios e complexos. Dois ou mais threads manipulando um controle podem forçar o controle a ficar em um estado inconsistente, levando a condições de disputa, deadlocks, congelamentos ou travamentos. Se você implementar o multithreading em seu aplicativo, certifique-se de chamar controles entre threads de maneira segura. Para mais informações, confira Práticas recomendadas de encadeamento gerenciado.
Há duas maneiras de chamar com segurança um controle do 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 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 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 detecta essas chamadas de thread não seguras gerando uma InvalidOperationException com a mensagem, a operação entre threads não é válida. Controle acessado de um thread diferente do thread em que ele foi criado. O InvalidOperationException sempre ocorre para chamadas entre threads não seguras durante a depuração do Visual Studio e pode ocorrer no runtime do aplicativo. Você deve corrigir o problema, mas pode desabilitar a exceção definindo a propriedade Control.CheckForIllegalCrossThreadCalls como false.
Chamadas seguras entre threads
Os aplicativos do Windows Forms seguem uma estrutura estrita semelhante ao contrato, semelhante a todas as outras estruturas de interface do usuário do Windows: todos os controles devem ser criados e acessados do mesmo thread. Isso é importante porque o Windows exige que os aplicativos forneçam um único thread dedicado para entregar mensagens do sistema. Sempre que o Gerenciador de Janelas do Windows detecta uma interação com uma janela de aplicativo, como uma tecla, um clique do mouse ou redimensionando 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 o thread da interface do usuário.
Como o código em execução em outro thread não pode acessar controles criados e gerenciados pelo thread de interface do usuário, o Windows Forms fornece maneiras de trabalhar com segurança com esses controles de outro thread, conforme demonstrado nos seguintes exemplos de código:
Exemplo: Usar 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 do thread principal para invocar o controle.
Exemplo: usar um BackgroundWorker
Um componente BackgroundWorker, que oferece um modelo controlado por eventos.
Exemplo: Usar Control.InvokeAsync (.NET 9 e posterior)
A partir do .NET 9, o Windows Forms inclui o InvokeAsync método, que fornece marshaling assíncrono 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. Não há suporte para ele no .NET Framework.
Noções básicas sobre a diferença: Invoke vs InvokeAsync
Control.Invoke (Envio – Bloqueio):
- Envia o delegado de forma síncrona para a fila de mensagens do thread de interface do usuário.
- O thread de chamada aguarda até que o thread da interface do usuário processe o delegado.
- Pode levar ao congelamento da interface do usuário quando o delegado enviado para a fila de mensagens estiver aguardando a chegada de uma mensagem (deadlock).
- Útil quando você tiver resultados prontos para exibição no thread da interface do usuário, por exemplo: desabilitar um botão ou definir o texto de um controle.
Control.InvokeAsync (Postagem – não bloqueio):
- Publica de forma assíncrona o delegado na fila de mensagens do thread de interface do usuário em vez de aguardar a conclusão da invocação.
- Retorna imediatamente sem bloquear o thread de chamada.
- Retorna um
Taskque pode ser aguardado para conclusão. - Ideal para cenários assíncronos e impede gargalos de thread da interface do usuário.
Vantagens de InvokeAsync
Control.InvokeAsync tem várias vantagens em relação ao método mais antigo Control.Invoke . Ele retorna um Task que você pode aguardar, fazendo com que funcione bem com código assíncrono e de espera. Ele também impede problemas comuns de deadlock que podem acontecer ao misturar código assíncrono com chamadas de invocação síncronas. Ao contrário Control.Invokedo método, o InvokeAsync método não bloqueia o thread de chamada, o que mantém seus aplicativos responsivos.
O método dá suporte ao cancelamento por meio CancellationTokende, portanto, você pode cancelar operações quando necessário. Ele também lida com exceções corretamente, passando-as de volta para 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 diretrizes abrangentes sobre manipuladores de eventos assíncronos e práticas recomendadas, consulte a visão geral de eventos.
Escolhendo a sobrecarga invokeAsync correta
Control.InvokeAsync fornece quatro sobrecargas para cenários diferentes:
| Sobrecarga | Caso de Uso | Example |
|---|---|---|
InvokeAsync(Action) |
Operação de sincronização, sem valor retornado. | Atualizar propriedades de controle. |
InvokeAsync<T>(Func<T>) |
Operação de sincronização, com valor retornado. | Obter o estado de controle. |
InvokeAsync(Func<CancellationToken, ValueTask>) |
Operação assíncrona, sem valor retornado.* | Atualizações de interface do usuário de execução longa. |
InvokeAsync<T>(Func<CancellationToken, ValueTask<T>>) |
Operação assíncrona, com valor retornado.* | Busca de dados assíncronos com resultado. |
*O Visual Basic não dá suporte à espera de um ValueTask.
O exemplo a seguir demonstra o uso InvokeAsync para atualizar com segurança controles 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 snippet de código anterior usou um método de extensão para converter um ValueTask em um Task. O código do método de extensão está disponível no GitHub.
Exemplo: usar o método Control.Invoke
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 que está realizando a chamada. Se eles forem diferentes, você deverá chamar o método Control.Invoke.
O WriteTextSafe permite definir a propriedade TextBox do controle Text como um novo valor. O método consulta InvokeRequired. Se InvokeRequired retornar true, WriteTextSafe chamará a si mesmo recursivamente, passando o método como um 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 Invoke é diferente, InvokeAsyncconsulte Noções básicas sobre a diferença: Invoke vs InvokeAsync.
Exemplo: usar um BackgroundWorker
Uma maneira fácil de implementar cenários de vários threading, garantindo que o acesso a um controle ou formulário seja executado somente no thread principal (thread da interface do usuário), é com o System.ComponentModel.BackgroundWorker componente, que usa um modelo controlado por eventos. O thread em segundo plano 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 do Windows Forms. Embora continuemos dando suporte a esse componente para compatibilidade com versões anteriores, ele trata apenas do descarregamento da carga de trabalho do processador do thread de interface do usuário para outro thread. Ele não lida com outros cenários assíncronos, como E/S de arquivo ou operações de rede em que o processador pode não estar funcionando ativamente.
Para programação assíncrona moderna, use async métodos com await em vez disso. Se você precisar descarregar explicitamente o trabalho com uso intensivo de 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 entre threads e eventos.
Para fazer uma chamada thread-safe usando BackgroundWorker, manipule o evento DoWork. Há dois eventos que o trabalho 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 trabalho 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 cada contagem. Ele usa o manipulador de eventos ProgressChanged para reportar o número de volta ao thread principal e para definir 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