vb.Net:在BackgroundWorker中创建UI

3
我正在开发一个应用程序,可以在3x3(即9个屏幕)视频墙上监控生产信息。我现在处理的其中一个屏幕集合会检索信息并格式化为屏幕显示。大约需要2秒钟才能检索和格式化这些数据(仅是估计,未实际测量)。因为涉及到9个屏幕,所以依次切换到该屏幕集合时有很明显的时间延迟。驱动这个视频墙的PC具有8个处理器核心,因此在一个处理器忙于处理所有这些工作时,还有很多处理能力闲置。
我的第一个想法是使用多线程。不幸的是,我对这个概念很陌生。我只用过一次。我尝试创建BackgroundWorker,并让DoWork例程生成我的UI。不幸的是,在我尝试创建UI元素(Dim grLine as New Grid)时,它会崩溃。我设法通过创建虚拟DoWork例程,并在RunWorkerCompleted例程中生成所有UI来解决这个问题。这确实使我的空白窗口立即显示出来,但在所有UI被渲染之前,我生成的任何UI都不会显示出来。
以下是我要做的事情的非常简洁版本:
For i As Integer = 1 to 9
    Dim win As New MyCustomWindow
    win.DisplayScreen = i   ' This function in MyCustomWindow sets the Bounds
    win.MyShow({1, 2})  ' Sample args
    Globals.VideoWall.Windows(i) = win
Next

MyCustomWindow类:

Class MyCustomWindow

    Public Sub MyShow(a() as Integer)
        Me.Show()  ' Has a "Loading..." TextBlock
        Dim bw as New ComponentModel.BackgroundWorker
        AddHandler bw.DoWork, AddressOf Generate_UI_DoWork
        AddHandler bw.RunWorkerCompleted, AddressOf Generate_UI_Complete
        bw.RunWorkerAsync(a) 
    End Sub

    Private Sub Generate_UI_DoWork((sender As Object, e As ComponentModel.DoWorkEventArgs)
        ' Pass our arguments to the Complete routine.
        e.Result = e.Argument
    End Sub

    Private Sub Generate_OpsMarket_Complete(sender As Object, e As ComponentModel.RunWorkerCompletedEventArgs)
        Dim IDs() as Integer
        IDs = e.Result

        Dim grLine As New Grid  ' We crash here if this code is in DoWork instead of RunWorkerCompleted
        For Each id As Integer In IDs
            grLine.RowDefinitions.Add(New RowDefinition)
            Dim txt as New TextBlock  ' For a header
            grLine.Children.Add(txt)

            grLine.RowDefinitions.Add(New RowDefinition)
            Dim MyCtrl as New MyCustomControl()
            MyCustomControl.GetData(id)
            grLine.Children.Add(MyCtrl.MyGrid)

            txt.Text = MyCtrl.Header
        Next

        txLoading.Visibility = Visibility.Hidden
        grRoot.Children.Add(grLine)
    End Sub
End Class

我尽力在代码中留下足够的细节,希望我的目标是明显的,但同时保持足够小,不会让人感到压抑。
编辑后添加:
大部分工作都在MyCustomControl.GetData(id)中完成...该子程序从Web服务器下载数据(以JSON格式),解析JSON,然后为Grid生成行(3)和列(30或31,具体取决于月份),并填充它从Web服务器接收到的数据。

欢迎来到多线程的世界!有几点需要注意。您希望充分利用8个逻辑核心,实现方法是使用8个或更多线程执行一堆工作,这些工作可以分为离散的工作单元。例如,您想计算1到100万之间所有整数的平方根。这些是可以相互独立地执行的单独工作单元,因此您可以让操作系统决定如何分配工作。单个后台工作者仍将在单个线程(单个核心)中运行,因此不会有太多好处。...续 - djv
在表单应用程序中使用BackgroundWorker的好处是允许在UI线程之外的线程上执行工作。这将允许UI继续刷新并响应输入,而不会通过处理非UI内容来阻塞它。您可能确实希望在非UI线程上执行计算,因为这是.NET中UI编程的非常重要的一部分。但是,为了使这些计算快速完成(通过利用8个逻辑核心),您需要在后台工作器中多线程执行正在进行的工作。如果有意义的话,我可以编写一个简单的示例来帮助您入门。 - djv
我不是非要让所有8个核心都保持繁忙状态,而是现在我所做的事情比应该需要的时间长得多。我正在为9个不同的屏幕生成UI,一个接一个地进行,每个屏幕大约需要2秒钟。在我的原始代码中,上面示例中Generate_OpsMarket_Complete内的所有内容都在MyShow中...通过将大部分代码移动到BackgroundWorker中,希望所有9个屏幕的UI可以并行生成,而不是顺序生成。 - StarDestroyer
应该将生成 UI 的耗时代码中与 UI 相关的部分剥离,只处理数据。然后可以在后台工作线程上运行它。后台工作线程应该将数据返回给 Generate_UI_Complete,以便用于生成 UI。 - djv
2个回答

2
你当前的BackgroundWorker实现没有任何好处,正如你自己所注意到的那样。你的主要问题是你当前的代码/逻辑严重依赖于UI控件,这使你受限于UI线程。因为只能在UI线程上创建/更新UI控件,所以当你在BackgroundWorker.DoWork处理程序中尝试创建/更新UI控件时,就会出现异常。
建议将检索监视信息的逻辑分离并行执行,然后使用已经格式化的数据在UI上创建/更新控件。
以下是原始/伪示例。
Class DataService
    Public Function GetData(ids As Integer()) As YourData
        ' Get data from web service
        ' Validate and Format it to YourData type or List(Of Yourdata)
        Return data
    End Function
End Class

Class MyCustomWindow

    Public Sub MyShow(a() as Integer)
        Me.Show()  ' Has a "Loading..." TextBlock
        Dim bw as New ComponentModel.BackgroundWorker
        AddHandler bw.DoWork, AddressOf Generate_UI_DoWork
        AddHandler bw.RunWorkerCompleted, AddressOf Generate_UI_Complete
        bw.RunWorkerAsync(a) 
    End Sub

    Private Sub Generate_UI_DoWork((sender As Object, e As ComponentModel.DoWorkEventArgs)
        Dim service As New DataService()
        Dim data = service.GetData(e.Argument)
        e.Result = data
    End Sub

    Private Sub Generate_OpsMarket_Complete(sender As Object, 
                                        e As ComponentModel.RunWorkerCompletedEventArgs)

    Dim data As Yourdata = DirectCast(e.Result, YourData)      
    'Update UI controls with already formatted data

    End Sub
End Class

关于“Sub从Web服务器下载数据”的更新
在这种情况下,您根本不需要使用多线程/并行。因为加载时间是等待响应的时间。在这种情况下,我的建议是使用async/await方法,这将释放UI线程(使其响应),而您正在等待来自Web服务的响应。

Class DataService
    Public Async Function GetDataAsync(ids As Integer()) As Task(Of YourData)
        Using client As HttpClient = New HttpClient()
            Dim response As HttpResponseMessage = Await client.GetAsync(yourUrl)
            If response.IsSuccessStatusCode = True Then
                Return Await response.Content.ReadAsAsync<YourData>()
            End If
        End Using
    End Function
End Class

那么在视图中,您就不需要使用 BackgroundWorker 了。

Class MyCustomWindow

    Public Async Sub MyShow(a() as Integer) As Task
        Me.Show()  ' Has a "Loading..." TextBlock

        Dim service As New DataService()
        Dim data As YourData = Await service.GetDataAsync(a)
        UpdateControlsWithData(data)
    End Sub

    Private Sub UpdateControlsWithData(data As YourData)
        ' Update controls with received data
    End Sub
End Class

我觉得我可能开始更好地理解这个了。所以看起来 DoWork 子程序在一个单独的线程上运行,并将其结果传递到 UI 线程上的 RunWorkerCompleted 子程序。所以,我在上面的伪代码中真正做的就是改变了阻塞发生的时间。我的希望是能够在一个单独的线程上生成 UI,然后将最终的 grLine 对象传递到 UI 线程中,以便插入到窗口中。 - StarDestroyer
在单独的线程上生成UI。不要在UI线程上执行耗时/资源密集型操作,而是在单独的线程上执行,然后将足够的信息传递回UI线程以生成UI。在大多数情况下,您甚至可以并行执行这些耗时操作,从而利用所有核心。 - djv
我可能完全走错了方向。我有一堆Debug.Print语句,所以我只是添加了一些时间来调试。下载和解析JSON需要30到70毫秒不等的时间。然后每个“行”(实际上是3个Grid.RowDefinitions)需要大约30毫秒才能添加。运行整个过程两次需要大约300毫秒。然后在下一个屏幕开始加载之前有大约400毫秒的休息时间。(续) - StarDestroyer
我假设在我将 grLine 添加到 grRoot 并且下一个窗口开始加载之间的这段时间是网格正在渲染的时候。如果我无法在不同的线程中生成/渲染UI,则通过使用单独的线程可以省去下载和解析JSON数据所需的70毫秒(或更少)的时间。 - StarDestroyer
30毫秒是添加一行非常长的时间。如果它是一个自定义控件,那么它可能在屏幕上绘制线条和字母之外还做了其他事情(计算/ tcpip / 任何其他操作?)。例如,我可以在5毫秒内向DataGridView中添加10k行。如果适用,您需要将所有与非UI相关的内容分离出来,包括该控件中的内容。 - djv

1
对于这个问题,以下是一些例子,它们执行9次500毫秒的数据操作,然后进行简单的UI操作。
第一个例子在后台线程上运行,但主循环按顺序运行。结尾处的消息框显示需要约4500毫秒,因为500毫秒的线程休眠按顺序运行。请注意,DoWork方法与UI无关。它使用两个线程:UI线程和一个后台工作者线程。由于它没有在UI上工作,所以在后台工作者正在工作时,表单是有响应的。
Private bw_single As New BackgroundWorker()

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    AddHandler bw_single.DoWork, AddressOf bw_single_DoWork
    AddHandler bw_single.RunWorkerCompleted, AddressOf bw_single_Complete
    bw_single.RunWorkerAsync()
End Sub

Private Sub bw_single_DoWork(sender As Object, e As DoWorkEventArgs)
    ' runs on background thread
    Dim data As New List(Of Integer)()
    Dim sw As New Stopwatch
    sw.Start()
    For i As Integer = 1 To 9
        ' simulate downloading data, etc.
        Threading.Thread.Sleep(500)
        data.Add(i)
    Next
    sw.Stop()
    e.Result = New Result(data, sw.ElapsedMilliseconds)
End Sub

Private Sub bw_single_Complete(sender As Object, e As RunWorkerCompletedEventArgs)
    RemoveHandler bw_single.DoWork, AddressOf bw_single_DoWork
    RemoveHandler bw_single.RunWorkerCompleted, AddressOf bw_single_Complete
    ' runs on UI thread
    Dim res = CType(e.Result, Result)
    Me.DataGridView1.DataSource = res.Data
    MessageBox.Show(
        String.Format("Performed on bw (single), took {0} ms, data: {1}", 
                      res.Elapsed, String.Join(", ", res.Data)))
End Sub

(这是保存后台工作者结果的类)
Private Class Result
    Public Property Data As IEnumerable(Of Integer)
    Public Property Elapsed As Long
    Public Sub New(data As IEnumerable(Of Integer), elapsed As Long)
        Me.Data = data
        Me.Elapsed = elapsed
    End Sub
End Class

第二个示例在后台线程上运行,但主循环并行运行。最后的消息框显示需要大约1000毫秒...为什么?因为我的机器和你的一样,有8个逻辑核心,但我们正在睡眠9次。因此,至少一个核心正在执行两个睡眠操作,并且这将限制整个操作。同样,UI有一个线程,后台工作者有一个线程,但对于并行循环,操作系统将从剩余的核心中为每个额外的线程分配CPU时间。UI响应迅速,完成相同任务所需的时间只是第一个示例的一小部分。
Private bw_multi As New BackgroundWorker()

Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    AddHandler bw_multi.DoWork, AddressOf bw_multi_DoWork
    AddHandler bw_multi.RunWorkerCompleted, AddressOf bw_multi_Complete
    bw_multi.RunWorkerAsync()
End Sub

Private Sub bw_multi_DoWork(sender As Object, e As DoWorkEventArgs)
    ' runs on background thread
    Dim data As New ConcurrentBag(Of Integer)()
    Dim sw As New Stopwatch
    sw.Start()
    Parallel.For(1, 9,
    Sub(i)
        data.Add(i)
        Threading.Thread.Sleep(500)
    End Sub)
    sw.Stop()
    e.Result = New Result(data, sw.ElapsedMilliseconds)
End Sub

Private Sub bw_multi_Complete(sender As Object, e As RunWorkerCompletedEventArgs)
    RemoveHandler bw_multi.DoWork, AddressOf bw_multi_DoWork
    RemoveHandler bw_multi.RunWorkerCompleted, AddressOf bw_multi_Complete
    ' runs on UI thread
    Dim res = CType(e.Result, Result)
    Me.DataGridView1.DataSource = res.Data
    MessageBox.Show(
        String.Format("Performed on bw (multi), took {0} ms, data: {1}",
                      res.Elapsed, String.Join(", ", res.Data)))
End Sub

由于上述两个示例利用后台工作线程来完成它们的工作,它们不会冻结UI线程。在UI中运行的唯一代码是按钮单击处理程序和RunWorkerCompleted处理程序。
最后,此示例仅使用单个UI线程。它将在运行4500秒时冻结UI。只是让你知道要避免什么...
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
    Dim data As New List(Of Integer)()
    Dim sw As New Stopwatch
    sw.Start()
    For i As Integer = 1 To 9
        ' simulate downloading data, etc.
        Threading.Thread.Sleep(500)
        data.Add(i)
    Next
    sw.Stop()
    Dim res = New Result(data, sw.ElapsedMilliseconds)
    Me.DataGridView1.DataSource = res.Data
    MessageBox.Show(
        String.Format("Performed on bw (single), took {0} ms, data: {1}",
                      res.Elapsed, String.Join(", ", res.Data)))
End Sub

简而言之,您应该学会如何将数据层与用户界面分离。请参见关注点分离和SO问题为什么有些开发人员很难做好UI设计?以及此问题我可以使用哪些UI设计原则(如“关注点分离”)来说服开发人员需要修复UI?


我在分离数据方面遇到的主要困难是UI是基于数据生成的。这就是为什么我在Code Behind中进行而不是XAML。但是,我已经开始将其分离出来。在这个过程中,我注意到在我的网格中为每个单元格设置.Background.BorderBrush.BorderThinkness是导致速度问题的最大因素。删除这三行代码可以将生成所有9个窗口的时间从12.9秒降至2.7秒。仅生成空的UI只会产生约0.5秒的差异。 - StarDestroyer
我想不出你能对此做些什么。加载UI的最快速度将是每个UI操作时间的总和。也许你可以将边框应用于模板单元格并复制该单元格?不确定你正在使用哪个网格类。 - djv
1
将两种不同的样式(偶数和奇数)添加到我的用户控件的“根”,并分配这些样式而不是显式设置.Background.BorderBursh.BorderThickness属性,这使得界面加载速度大大提升。将数据载入UI中也会有所帮助,但这一次单一的变化已经产生了巨大的影响。非常感谢您的所有帮助。 - StarDestroyer
很好了解,也许您可以在问题中添加一些关于您使用的控件类以及如何改善情况的信息。 - djv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接