2016-11-11 100 views
3

我正在研究一個應用程序,以便能夠監視3x3(如此9屏幕)視頻牆上的生產信息。我正在處理的屏幕集之一現在檢索信息,然後在屏幕上顯示格式。它需要大約2秒鐘來檢索和格式化這些數據(只是粗略的猜測,而不是實際測量)。因爲它一個接一個地顯示9個屏幕,所以切換到這個屏幕集有非常明顯的時間。驅動這個視頻牆的個人電腦有8個處理核心,所以儘管一個處理器正在努力完成所有這些工作,但仍有大量處理能力處於閒置狀態。vb.Net:在BackgroundWorker中創建UI

我的第一個想法是我需要使用多線程。不幸的是,我對這個概念很陌生。我真的只用過一次。我試着創建一個BackgroundWorker並讓DoWork例程生成我的UI。不幸的是,它第一次嘗試創建UI元素時崩潰(Dim grLine as New Grid)。我確實設法通過虛擬DoWork例程並在RunWorkerCompleted例程中生成所有我的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,然後生成的行(3)和列(30或31,取決於月份),並填寫從Web服務器收到的數據。

+0

歡迎來到多線程世界!一些事情。您希望充分利用8個邏輯內核,而要做到這一點的方法是使用8個或更多線程來完成一系列可以分離成不同工作單元的工作。例如,你想計算從1到100萬的所有整數的sqrt。這些是不同的工作單元,每個工作單元都可以獨立執行,因此您可以讓操作系統決定如何分配工作。單一的後臺工作者仍然只能在單線程(單核)下運行,因此在那裏沒有太多好處。續... – djv

+0

表單應用程序中的背景工作者的好處是允許在UI線程以外的線程上完成工作。這將允許UI繼續刷新並響應輸入,而不會通過處理非UI內容堵塞它。您可能確實想在非UI線程上進行計算,因爲它是.NET中UI編程的一個非常重要的部分。但爲了快速完成這些計算(通過利用8個邏輯內核),您希望多線程工作在後臺工作中完成。如果這是有道理的,我可以寫一個簡單的例子讓你走。 – djv

+0

這並不是因爲我需要保持所有8個內核的繁忙,而是我現在做事情的方式花費的時間比應該需要的時間長。我爲9個不同的屏幕一個接一個地生成用戶界面,每個屏幕大約需要2秒。在我最初的代碼中,上面'Generate_OpsMarket_Complete'內的示例中的所有內容都在'MyShow'中......通過將大量代碼移動到BackgroundWorker,希望所有9屏幕的UI可以並行生成而不是按順序生成。 – StarDestroyer

回答

1

對於它的價值,這是一些做9×500ms的數據操作,那麼簡單的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秒時凍結用戶界面。只是讓你知道該怎麼避...

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 

總而言之,你應該弄清楚如何從UI分離數據層。請參閱separation of concerns和SO問題,Why is good UI design so hard for some Developers?和這一個What UI design principles like 「separation of concerns」 can I use to convince developers that the UI needs fixing?

+0

我在分離數據時遇到的主要困難是UI是基於數據生成的。這就是爲什麼我在Code Behind而不是XAML中執行它的原因。但是,我確實開始了分離的過程。在這樣做的過程中,我注意到在我的網格中的每個單元上設置'.Background','.BorderBrush'和'.BorderThinkness'是我速度問題的最大貢獻者。去除這三條線需要12.9到2.7秒來生成全部9個窗口。生成空白的用戶界面只會產生大約0.5的差異。 – StarDestroyer

+0

我想不出你可以做什麼。您可以加載UI的速度最快將是每個UI操作時間的總和。也許你可以將邊框應用到模板單元格並複製單元格?不確定你正在使用的網格類。 – djv

+1

爲我的用戶控件的「根」添加兩種不同的樣式(偶數和奇數),並指定這些樣式,而不是明確設置'.Background','.BorderBursh'和'.BorderThickness'屬性,這兩者有着天壤之別。整組屏幕現在加載大約3.1秒。將數據負載分解到UI中也會有所幫助,但這一改變已經造成了巨大的變化。非常感謝所有的幫助。 – StarDestroyer

2

您的當前BackgroundWorker實施給你沒有任何好處,正如你注意到你的自我。
您的主要問題是您的當前代碼/邏輯嚴重依賴於UI控件。你用UI線程嚴格你。因爲創建/更新UI控件只能在UI線程上完成 - 這就是爲什麼在嘗試創建/更新BackgroundWorker.DoWork處理程序中的UI控件時收到Exception。

建議將並行檢索監視信息的邏輯分開,然後您可以使用已格式化的數據在UI上創建/更新控制。

這是原始/僞例如在次下載從Web服務器
數據

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 

更新在這種情況下,你不需要多線程/並行的。因爲你加載時間正在等待響應時間。在這種情況下,我的建議將使用async/await的方法,它將在等待Web服務響應時釋放UI線程(使其響應)。

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 
+0

我想我可能會開始更好地理解這一點。所以它看起來像'DoWork' Sub作爲一個單獨的線程運行,並將其結果傳遞給UI線程上的'RunWorkerCompleted'子。所以我真正用上面的僞代碼完成的事情是在發生阻塞時發生移位。我的希望是能夠在單獨的線程上生成UI,然後將最終的'grLine'對象傳遞給UI線程以插入窗口。 – StarDestroyer

+0

'在單獨的線程上生成用戶界面'沒有。您在單獨的線程上執行時間/資源消耗操作,然後將足夠的信息傳遞迴UI線程以生成UI。在大多數情況下,您甚至可以並行執行這些耗時的操作,您可以利用所有內核。 – djv

+0

然後,我可能會完全錯誤地發出錯誤的樹。我有一大堆'Debug.Print'語句,所以我只是給他們添加了一些時間。它需要30到70毫秒的時間來下載和解析JSON。然後每個「行」(實際上是3個'Grid.RowDefinitions')需要大約30ms才能添加。運行這整個過程需要大約300ms。然後在下一個屏幕開始加載之前有大約400毫秒的中斷。 (續) – StarDestroyer