Como deixar o código funcionar sem problemas usando timers e diferentes threads

Sep 21 2020

Estou tentando evitar que a GUI congele , por causa de um intervalo de timer baixo e muito para processar no Timer.Tickmanipulador de eventos.
Estou pesquisando no Google há algum tempo e entendi que não posso atualizar a IU a partir de qualquer outro thread que não seja o thread da IU

Então, e se você estiver usando muitos controles sob Timer1.Tick?
Como posso atualizar um rótulo quando os dados são baixados com WebClient com um cronômetro, você não quer diminuir muito o intervalo e manter a IU responsiva ao mesmo tempo?

Eu recebo exceções de violação de Cross Thread quando acesso elementos da interface do usuário, um ListBox1 e um RichTextBox.

Qual é a maneira correta de atualizar a IU com um cronômetro e / ou Thread sem causar exceções de ameaças cruzadas?

Respostas

2 Jimi Sep 22 2020 at 01:24

Você tem maneiras diferentes de atualizar os elementos da UI a partir de um Thread diferente do UI Thread.
Você pode usar o InvokeRequired/Invoke()padrão ( meh ), chamar o método assíncrono BeginInvoke() , Post()para o SynchronizationContext , talvez misturado com um AsyncOperation + AsyncOperationManager (estilo BackGroundWorker sólido), usar um retorno de chamada assíncrono etc.

Também existe a Progressclasse e sua IProgressinterface.
Esta classe fornece uma maneira bastante simplificada de capturar SynchronizationContextonde o objeto de classe é criado e de Post()volta ao contexto de execução capturado.
O Progress<T>delegado criado no UI Thread é chamado nesse contexto. Precisamos apenas passar o Progress<T>objeto e lidar com as notificações que recebemos por meio do Action<T>delegado.
Você está baixando e manipulando uma string, então seu Progress<T>objeto será um Progress(Of String): portanto, ele retornará uma string para você.

O Timer é substituído por uma Tarefa que executa seu código e também atrasa suas ações por um Intervalo que você pode especificar, como com um Timer, usando Task.Delay ([Interval]) entre cada ação . Existe um StopWatch que mede o tempo que um download realmente leva e ajusta o Delay com base no intervalo especificado (não é uma coisa de precisão , de qualquer maneira).


► No código de amostra, a Tarefa de download pode ser iniciada e interrompida usando os métodos StartDownload()e StopDownload()de uma classe auxiliar.
O StopDownload()método está aguardando, executa o cancelamento das tarefas atuais e descarta os objetos descartáveis ​​usados.

► Substituí WebClient por HttpClient, ainda é bastante simples de usar (pelo menos em seu básico), fornece métodos assíncronos que suportam um CancellationToken(embora um download em andamento exija algum tempo para cancelar, mas é tratado aqui).

► Um clique de Botão inicializa e inicia os downloads cronometrados e outro para interrompê-lo (mas você pode chamar o StopDownload()método quando o Form fechar, ou, bem, sempre que precisar).

► O Progress<T>delegado é apenas um Lambda aqui: não há muito o que fazer, basta preencher um ListBox e rolar um RichTextBox. Você pode inicializar o objeto da classe helper (é nomeado MyDownloader: claro que você vai escolher outro nome, esse aqui é ridículo) e chamar o seu StartDownload()método, passando o objeto Progress, o Uri e o Interval entre cada download.

Private downloader As MyDownloader = Nothing

Private Sub btnStartDownload_Click(sender As Object, e As EventArgs) Handles btnStartDownload.Click
    Dim progress = New Progress(Of String)(
        Sub(data)
            ' We're on the UI Thread here
            ListBox1.Items.Clear()
            ListBox1.Items.AddRange(Split(data, vbLf))
            RichTextBox1.SelectionStart = RichTextBox1.TextLength
        End Sub)

    Dim url As Uri = New Uri("https://SomeAddress.com")
    downloader = New MyDownloader()
    ' Download from url every 1 second and report back to the progress delegate
    downloader.StartDownload(progress, url, 1)

Private Async Sub btnStopDownload_Click(sender As Object, e As EventArgs) Handles btnStopDownload.Click
    Await downloader.StopDownload()
End Sub

A classe auxiliar:

Imports System.Diagnostics
Imports System.Net
Imports System.Net.Http
Imports System.Text.RegularExpressions

Public Class MyDownloader
    Private Shared ReadOnly client As New HttpClient()
    Private ReadOnly cts As CancellationTokenSource = New CancellationTokenSource()
    Private interval As Integer = 0

    Public Sub StartDownload(progress As IProgress(Of String), url As Uri, intervalSeconds As Integer)
        interval = intervalSeconds * 1000
        Task.Run(Function() DownloadAsync(progress, url, cts.Token))
    End Sub

    Private Async Function DownloadAsync(progress As IProgress(Of String), url As Uri, token As CancellationToken) As Task
        Dim responseData As String = String.Empty
        Dim pattern As String = "<(?:[^>=]|='[^']*'|=""[^""]*""|=[^'""][^\s>]*)*>"
        Dim downloadTimeWatch As Stopwatch = New Stopwatch()
        downloadTimeWatch.Start()
        Do
            If cts.IsCancellationRequested Then Return
            Try
                Using response = Await client.GetAsync(url, HttpCompletionOption.ResponseContentRead, token)
                    responseData = Await response.Content.ReadAsStringAsync()
                    responseData = WebUtility.HtmlDecode(Regex.Replace(responseData, pattern, ""))
                End Using
                progress.Report(responseData)

                Dim delay = interval - CInt(downloadTimeWatch.ElapsedMilliseconds)
                Await Task.Delay(If(delay <= 0, 10, delay), token)
                downloadTimeWatch.Restart()
            Catch tcEx As TaskCanceledException
                ' Don't care - catch a cancellation request
                Debug.Print(tcEx.Message)
            Catch wEx As WebException
                ' Internet connection failed? Internal server error? See what to do
                Debug.Print(wEx.Message)
            End Try
        Loop
    End Function

    Public Async Function StopDownload() As Task
        Try
            cts.Cancel()
            client?.CancelPendingRequests()
            Await Task.Delay(interval)
        Finally
            client?.Dispose()
            cts?.Dispose()
        End Try
    End Function
End Class
1 dbasnett Sep 21 2020 at 17:43

Os seus acessos à caixa de listagem e richtextbox devem ser executados no thread de IU A maneira mais fácil de fazer isso é assim.

                                     Me.Invoke(Sub()
                                                   ListBox1.Items.Clear()
                                                   ListBox1.Items.AddRange(Split(clientdecode, vbLf))
                                                   RichTextBox1.SelectionStart() = RichTextBox1.TextLength
                                                   RichTextBox1.ScrollToCaret()
                                               End Sub)