Come far funzionare il codice senza problemi utilizzando timer e thread diversi

Sep 21 2020

Sto cercando di impedire il blocco della GUI , a causa di un intervallo di timer basso e troppo da elaborare nel Timer.Tickgestore eventi.
Ho cercato su Google un po 'di tempo e ho capito che non posso aggiornare l'interfaccia utente da nessun altro thread diverso dal thread dell'interfaccia utente.

Quindi, cosa succede se stai usando molti controlli sotto Timer1.Tick?
Come posso aggiornare un'etichetta quando i dati vengono scaricati con WebClient con un timer, non vuoi abbassare troppo l'intervallo e mantenere allo stesso tempo reattiva l'interfaccia utente?

Ricevo eccezioni di violazione di Cross Thread quando accedo a elementi dell'interfaccia utente, un ListBox1 e un RichTextBox.

Qual è il modo corretto per aggiornare l'interfaccia utente con un timer e / o un thread senza causare eccezioni per minacce incrociate?

Risposte

2 Jimi Sep 22 2020 at 01:24

Esistono diversi modi per aggiornare gli elementi dell'interfaccia utente da un thread diverso dal thread dell'interfaccia utente.
Puoi usare il InvokeRequired/Invoke()pattern ( meh ), chiamare il metodo asincrono BeginInvoke() , Post()al SynchronizationContext , magari mescolato con un AsyncOperation + AsyncOperationManager (solido stile BackGroundWorker), usare un callback asincrono ecc.

C'è anche la Progressclasse e la sua IProgressinterfaccia.
Questa classe fornisce un modo abbastanza semplificato per catturare il punto in SynchronizationContextcui viene creato l'oggetto della classe e Post()tornare al contesto di esecuzione acquisito.
Il Progress<T>delegato creato nel thread dell'interfaccia utente viene chiamato in quel contesto. Dobbiamo solo passare l' Progress<T>oggetto e gestire le notifiche che riceviamo tramite il Action<T>delegato.
Stai scaricando e gestendo una stringa, quindi il tuo Progress<T>oggetto sarà un Progress(Of String): quindi, ti restituirà una stringa.

Il timer viene sostituito da un'attività che esegue il codice e ritarda anche le sue azioni da un intervallo che è possibile specificare, come con un timer, qui utilizzando Task.Delay ([Interval]) tra ogni azione . C'è un StopWatch che misura il tempo effettivamente impiegato da un download e regola il ritardo in base all'intervallo specificato (non è una cosa di precisione , comunque).


► Nel codice di esempio, l'attività di download può essere avviata e interrotta utilizzando i metodi StartDownload()e StopDownload()di una classe helper.
Il StopDownload()metodo è attende, esegue l'annullamento delle attività correnti e elimina gli oggetti usa e getta utilizzati.

► Ho sostituito WebClient con HttpClient, è ancora abbastanza semplice da usare (almeno nelle sue basi), fornisce metodi asincroni che supportano a CancellationToken(anche se un download in corso richiede un po 'di tempo per essere annullato, ma è gestito qui).

► Un clic del pulsante inizializza e avvia i download a tempo e un altro lo interrompe (ma puoi chiamare il StopDownload()metodo quando il modulo si chiude, o, beh, ogni volta che ne hai bisogno).

► Il Progress<T>delegato è solo un Lambda qui: non c'è molto da fare, basta riempire un ListBox e scorrere un RichTextBox. Puoi inizializzare l'oggetto della classe helper (si chiama MyDownloader: ovviamente sceglierai un altro nome, questo è ridicolo) e chiamare il suo StartDownload()metodo, passando l'oggetto Progress, l'Uri e l'Interval tra ogni 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

La classe helper:

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

Gli accessi alla casella di riepilogo e alla casella di testo RTF devono essere eseguiti sul thread dell'interfaccia utente. Il modo più semplice per farlo è così.

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