Compartilhar via


Como lidar com operações entre threads com controles

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)

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 Task que 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