如何让代码运行顺畅的使用定时器和不同的线程

How to let the code run smoothly using timers and different threads

我正试图阻止 GUI 冻结,因为计时器间隔很短并且 Timer.Tick 事件中有太多要处理的东西处理程序。
我在谷歌上搜索了一段时间,我知道我无法从 UI 线程以外的任何其他线程更新 UI。

那么,如果您在 Timer1.Tick 下使用大量控件怎么办?
当使用带有计时器的 WebClient 下载数据时,我如何更新标签,您不想降低间隔太多并同时保持 UI 响应?

当我访问 UI 个元素、一个 ListBox1 和一个 RichTextBox 时,我收到跨线程冲突异常。

在不导致交叉威胁异常的情况下,使用计时器 and/or 更新 UI 线程的正确方法是什么?

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

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

您可以通过不同的方式从 UI 线程以外的线程更新 UI 元素。
可以使用InvokeRequired/Invoke()模式(meh),调用异步BeginInvoke()方法,Post() to the SynchronizationContext, maybe mixed with an AsyncOperation + AsyncOperationManager( solid BackGroundWorker 样式),使用异步回调等

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

Timer 被一个 Task 取代,Task 执行您的代码,并通过您可以指定的 Interval 延迟其操作,与 Timer 一样,这里使用 Task.Delay([Interval]) between each action. There's a StopWatch 测量下载实际花费的时间和根据指定的间隔调整延迟(无论如何,这不是精度)。


▶ 在示例代码中,可以使用 StartDownload()StopDownload()[=71 来启动和停止下载任务=] 助手的方法 class.
StopDownload() 方法是可等待的,它执行当前任务的取消并处理使用的一次性对象。

▶ 我已经用 HttpClient 替换了 WebClient,它使用起来仍然很简单,它提供了支持 CancellationToken 的异步方法(尽管正在进行的下载需要 一些时间取消,这里处理。

▶ 单击按钮初始化并开始定时下载,然后单击另一个按钮停止下载(但您可以在表单关闭时调用 StopDownload() 方法,或者,好吧,只要您需要)。

Progress<T> 委托在这里只是一个 Lambda:没什么可做的,只需填充一个 ListBox 并滚动一个 RichTextBox。
你可以初始化helperclass对象(它的名字叫MyDownloader:当然你会取另一个名字,这个很可笑)并调用它的StartDownload() 方法,在每次下载之间传递 Progress<T> 对象、UriInterval

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

帮手class:

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

Public Class MyDownloader
    Implements IDisposable

    Private Shared client As New HttpClient()
    Private cts As CancellationTokenSource = Nothing
    Private interval As Integer = 0
    Private disposed As Boolean

    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
        token.ThrowIfCancellationRequested()

        Dim responseData As String = String.Empty
        Dim pattern As String = "<(?:[^>=]|='[^']*'|=""[^""]*""|=[^'""][^\s>]*)*>"
        Dim downloadTimeWatch As Stopwatch = New Stopwatch()
        downloadTimeWatch.Start()
        Do
            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

    Protected Overridable Sub Dispose(disposing As Boolean)
        If Not disposed AndAlso disposing Then
            client?.Dispose()
            client = Nothing
        End If
        disposed = True
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
    End Sub
End Class