如何使用计时器和不同的线程使代码平稳运行

问题描述

由于计时器间隔太短并且无法在Timer.Tick事件处理程序中进行处理,因此我试图阻止GUI 冻结
我已经搜寻了一段时间,我知道我无法从UI线程以外的任何其他线程更新UI。

那么,如果您在Timer1.Tick下使用大量控件怎么办?
当您使用WebClient通过计时器下载数据时,又不想缩短间隔时间并保持UI响应速度,又该如何更新Label?

当我访问UI元素,ListBox1和RichTextBox时,我收到了违反线程冲突的异常。

用计时器和/或线程更新UI而不引起交叉威胁异常的正确方法是什么?

解决方法

除了UI线程外,您还有其他方法可以从线程中更新UI元素。
您可以使用InvokeRequired/Invoke()模式( meh ),调用异步 BeginInvoke()方法,将Post()调用到SynchronizationContext ,可能与AsyncOperation + AsyncOperationManager(BackGroundWorker风格)混合,使用异步回调等。

还有Progress<T>类及其IProgress<T>接口。
此类提供了一种非常简单的方法来捕获创建类对象的SynchronizationContext并将Post()返回捕获的执行上下文。
在该上下文中调用在UI线程中创建的Progress<T>委托。我们只需要传递Progress<T>对象并处理通过Action<T>委托收到的通知。
您正在下载和处理字符串,因此您的Progress<T>对象将是 Progress(Of String) :因此,它将向您返回一个字符串。

计时器被执行任务的任务所代替,该任务还执行您的代码,并通过您可以指定的时间间隔来延迟其动作,就像使用计时器一样,此处在每个动作之间使用Task.Delay([Interval])。有一个StopWatch可以测量下载实际花费的时间,并根据指定的间隔调整延迟(无论如何,这都不是 precision )。>


►在示例代码中,可以使用助手类的 StartDownload() StopDownload() 方法来启动和停止下载Task。
StopDownload()方法是可以等待的,它可以执行当前任务的取消操作并处理所用的一次性对象。

►我已经用HttpClient替换了WebClient,它仍然非常易于使用(至少在其基础上),它提供了支持CancellationToken的异步方法(尽管正在进行的下载需要一段时间) 取消,但在此处处理)。

►单击一次Button会初始化并开始定时下载,而另一次则停止下载(但是您可以在窗体关闭时或需要时调用StopDownload()方法)。

Progress<T>委托在这里只是一个Lambda:没有什么可做的,只需填充一个ListBox并滚动一个RichTextBox。您可以初始化助手类对象(其名称为 MyDownloader :当然,您会选择另一个名称,这个名称很荒谬)并调用其StartDownload()方法,并传递Progress对象,Uri和每次下载之间的间隔。

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

助手类:

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,cts.Token))
    End Sub

    Private Async Function DownloadAsync(progress As IProgress(Of String),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
,

您的列表框和Richtextbox访问必须在UI线程上运行。这样最简单的方法。

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