0

我试图防止 GUI冻结Timer.Tick,因为计时器间隔低并且在事件处理程序中处理太多。
我已经用谷歌搜索了一段时间,我了解到我无法从 UI 线程以外的任何其他线程更新 UI。

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

当我访问 UI 元素、ListBox1 和 RichTextBox 时,我收到了跨线程违规异常。

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

4

2 回答 2

2

您有不同的方法从 UI 线程以外的线程更新 UI 元素。
您可以使用InvokeRequired/Invoke()模式 ( meh ),调用异步 BeginInvoke()方法,Post()调用SynchronizationContext,可能与AsyncOperation + AsyncOperationManager (solid BackGroundWorker 样式)混合,使用异步回调等。

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

Timer 被一个 Task 替换,它执行您的代码,并通过您可以指定的 Interval 延迟其操作,就像 Timer 一样,在每个操作之间使用Task.Delay([Interval])。有一个秒表可以测量下载实际花费的时间,并根据指定的间隔调整延迟(无论如何,这不是一个精确的东西)。


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

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

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

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

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
    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
于 2020-09-22T01:24:15.050 回答
1

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

                                     Me.Invoke(Sub()
                                                   ListBox1.Items.Clear()
                                                   ListBox1.Items.AddRange(Split(clientdecode, vbLf))
                                                   RichTextBox1.SelectionStart() = RichTextBox1.TextLength
                                                   RichTextBox1.ScrollToCaret()
                                               End Sub)
于 2020-09-21T17:43:38.030 回答