2016-11-11 1 views
3

Je travaille sur une application pour pouvoir surveiller les informations de production sur un mur vidéo 3x3 (donc 9 écrans). Un des jeux d'écran sur lesquels je travaille en ce moment récupère des informations et les formats pour les afficher sur l'écran. Il faut environ 2 secondes pour récupérer et formater ces données (juste une estimation approximative, pas réellement mesurée). Parce qu'il fait 9 écran, l'un après l'autre, il y a un temps très visible pour passer à ce jeu d'écran. Le PC qui pilote ce mur vidéo a 8 cœurs de traitement, alors qu'un processeur est en train de faire tout ce travail, il y a beaucoup de puissance de traitement qui reste inactive. Ma première pensée est que j'ai besoin d'utiliser le multi-threading. Malheureusement, je suis très nouveau à ce concept. Je ne l'ai vraiment utilisé qu'une seule fois. J'ai essayé de créer un BackgroundWorker et d'avoir la routine DoWork pour générer mon interface utilisateur. Malheureusement, il se bloque la première fois que j'essaie de créer un élément d'interface utilisateur (Dim grLine as New Grid). J'ai réussi à contourner cela en ayant une routine factice DoWork et générant toute mon interface utilisateur dans la routine RunWorkerCompleted. Cela permet à ma fenêtre vierge d'apparaître immédiatement, mais aucune des interfaces que je génère n'apparaît tant que tout n'a pas été rendu.vb.Net: Création d'une interface utilisateur dans un BackgroundWorker

est ici une version très nettoyé de ce que je suis en train de faire:

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 

Classe 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 

J'ai essayé de laisser suffisamment de détails dans le code si nous espérons qu'il » Sois évident ce que j'essaie d'accomplir, mais en le gardant assez petit pour ne pas être écrasant.

Edité ajouter:
La majeure partie du travail se produit dans MyCustomControl.GetData(id) ... que sous télécharge les données à partir d'un serveur Web (au format JSON), analyse le JSON, puis génère les lignes (3) et colonnes (30 ou 31, selon le mois) pour la grille et remplit les données reçues du serveur Web.

+0

Bienvenue dans le monde du multithreading! Un couple de choses. Vous voulez profiter pleinement de 8 cœurs logiques, et la manière de le faire est d'utiliser 8 threads ou plus pour faire un tas de travail qui peut être séparé en unités de travail discrètes. Par exemple, vous voulez calculer le sqrt de tous les entiers de 1 à 1 million. Ce sont des unités de travail distinctes qui peuvent chacune être effectuées indépendamment les unes des autres, de sorte que vous laissez le système d'exploitation décider comment diviser le travail. Un seul travailleur d'arrière-plan continuera à fonctionner dans un seul thread (single core), donc pas beaucoup d'avantages là-bas. suite ... – djv

+0

L'avantage d'un arrière-plan dans une application de formulaires est de permettre de travailler sur un thread autre que le thread UI. Cela permettra à l'interface utilisateur de continuer à actualiser et à répondre aux entrées sans l'encombrer en traitant des éléments non liés à l'interface utilisateur. Vous voulez probablement faire vos calculs sur un thread non-UI, car c'est une partie très importante de la programmation de l'interface utilisateur dans .NET. Mais pour que ces calculs soient faits rapidement, (en utilisant 8 cœurs logiques), vous voudriez multi-thread le travail en cours dans le travailleur d'arrière-plan. Si cela est logique, je peux écrire un exemple simple pour vous aider à démarrer. – djv

+0

Ce n'est pas tellement que j'ai besoin de garder tous les 8 cœurs occupés, c'est que la façon dont je fais les choses maintenant prend beaucoup plus de temps que nécessaire. Je génère l'interface utilisateur pour 9 écrans différents, l'un après l'autre, et chaque écran prend environ 2 secondes.Dans mon code d'origine, tout ce qui était dans 'Generate_OpsMarket_Complete' dans' Generate_OpsMarket_Complete' était dans 'MyShow' ... en déplaçant la majeure partie du code dans un BackgroundWorker, l'espoir était que l'interface utilisateur de tous les écrans 9 puisse générer en parallèle au lieu de séquentiellement . – StarDestroyer

Répondre

1

Pour ce que ça vaut la peine, voici quelques exemples qui font 9 x 500ms des opérations de données, puis le fonctionnement de l'interface utilisateur simple.

Le premier exemple s'exécute sur un thread d'arrière-plan mais la boucle principale s'exécute en séquence. La boîte de message à la fin montre que cela prend environ 4500 ms à cause du thread de 500 ms le sommeil est exécuté séquentiellement. Notez que la méthode DoWork n'a rien à voir avec l'interface utilisateur. Il utilise deux threads: le thread d'interface utilisateur et un worker d'arrière-plan. Comme il ne fonctionne pas sur l'interface utilisateur, le formulaire est réactif pendant que l'arrière-plan travaille.

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 

(Ceci est la classe qui contient le résultat de l'ouvrier d'arrière-plan)

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 

Le second exemple est exécuté sur un fil de fond, mais la boucle principale se déroule en parallèle. Le messagebox à la fin montre qu'il faut environ 1000 ms ... pourquoi? Parce que ma machine comme la vôtre a 8 cœurs logiques mais nous dormons 9 fois. Donc, au moins un noyau fait deux sessions, ce qui va bloquer toute l'opération. Encore une fois, il y a un thread pour l'interface utilisateur, un pour le worker d'arrière-plan, mais pour la boucle parallèle, le système d'exploitation alloue le temps CPU des cœurs restants à chaque thread supplémentaire. L'interface utilisateur est sensible et il faut une fraction du temps du premier exemple de faire la même chose

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 

Depuis les deux exemples ci-dessus utilisent les travailleurs de base pour faire leur travail, ils ne seront pas geler le thread d'interface utilisateur. Le seul code qui s'exécute sur l'interface utilisateur se trouve dans les gestionnaires de clics sur les boutons et dans le gestionnaire RunWorkerCompleted. Enfin, cet exemple n'utilise qu'un seul thread d'interface utilisateur. Il va geler l'interface utilisateur pendant qu'il fonctionne pendant 4500 secondes. Juste pour que vous savez ce qu'il faut éviter ...

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 

Sommairement, vous devez savoir comment séparer la couche de données de l'interface utilisateur. Voir separation of concerns et la question SO, Why is good UI design so hard for some Developers?, et celui-ci What UI design principles like “separation of concerns” can I use to convince developers that the UI needs fixing?

+0

La principale difficulté que j'ai avec la séparation des données est que l'interface utilisateur est générée sur la base des données. C'est pourquoi je le fais dans Code Behind plutôt que XAML. Mais, j'ai commencé le processus de séparation. En procédant ainsi, j'ai remarqué que la définition de '.Background',' .BorderBrush', et '.BorderThinkness' sur chacune des cellules de ma grille est le plus gros contributeur à mon problème de vitesse. Retrait de ces trois lignes va de 12,9 à 2,7 secondes pour générer toutes les 9 fenêtres. Générer l'UI vide ne fait que 0,5s de différence. – StarDestroyer

+0

Je ne peux pas penser à tout ce que vous pouvez faire à ce sujet. Le plus rapide, vous pouvez charger l'interface utilisateur serait la somme de l'heure de chaque opération de l'interface utilisateur. Peut-être que vous pouvez appliquer les bordures à une cellule de modèle et dupliquer la cellule? Vous n'êtes pas sûr de la classe de grille que vous utilisez. – djv

+1

L'ajout de deux styles différents (pair et impair) à la "racine" de mon contrôle utilisateur et l'affectation de ces styles au lieu de définir explicitement les propriétés '.Background',' .BorderBursh' et '.BorderThickness' ont fait toute la différence. L'ensemble des écrans se charge maintenant en environ 3,1 secondes. La répartition de la charge de données dans l'interface utilisateur aidera également, mais ce changement unique a déjà fait une énorme différence. Merci beaucoup pour toute l'aide. – StarDestroyer

2

Votre implémentation BackgroundWorker actuelle ne vous donne aucun avantage, comme vous l'avez remarqué.
Votre problème principal est que votre code/logique actuel dépend étroitement des contrôles de l'interface utilisateur. Qui vous stricte avec le fil de l'interface utilisateur. Parce que la création/mise à jour des contrôles de l'interface utilisateur peut être effectuée uniquement sur le thread UI - c'est pourquoi vous avez Exception lors de la tentative de créer/mettre à jour les contrôles de l'interface utilisateur dans le gestionnaire BackgroundWorker.DoWork.

Suggérer de séparer la logique qui récupère les informations de surveillance en parallèle et ensuite vous pouvez créer/mettre à jour le contrôle sur l'interface utilisateur avec des données déjà formatées.

Ceci est par exemple brut/pseudo

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 

Mise à jour sur sous télécharge les données à partir d'un serveur Web
Dans ce cas, vous n'avez pas besoin multi-threading/parallèle à tous. Parce que le temps de chargement est le temps d'attente pour la réponse. Dans ce cas, mon conseil utilisera l'approche async/await, qui libérera le thread de l'interface utilisateur (rendez-le réactif) pendant que vous attendez la réponse du service 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 

Puis dans la vue que vous n'avez pas besoin 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

Je pense que je commence peut-être à mieux comprendre cela. Il semble donc que le sous-programme 'DoWork' s'exécute en tant que thread séparé et transmet son résultat au sous-programme' RunWorkerCompleted' sur le thread de l'interface utilisateur. Donc, tout ce que j'ai vraiment fait avec mon pseudo code ci-dessus est le décalage lorsque le blocage se produit. Mon espoir était de pouvoir générer l'interface utilisateur sur un thread séparé, puis passer l'objet 'grLine' final au thread UI à insérer dans la fenêtre. – StarDestroyer

+0

'générer l'interface utilisateur sur un thread séparé' non. Vous effectuez les opérations consommatrices de temps/ressources sur un thread distinct, puis transmettez suffisamment d'informations au thread d'interface utilisateur pour générer l'interface utilisateur. Vous pouvez même faire ces opérations longues en parallèle dans la plupart des cas, où vous pourriez tirer parti de tous vos cœurs. – djv

+0

Je peux aboyer complètement le mauvais arbre, alors. J'ai une série d'instructions 'Debug.Print', donc je leur ai juste ajouté du temps. Il faut entre 30 et 70 millisecondes pour télécharger et analyser le fichier JSON. Ensuite, chaque 'ligne' (qui est en fait 3 'Grid.RowDefinitions') prend environ 30ms à ajouter. Pour exécuter ce processus deux fois prend environ 300 ms. Il y a ensuite une pause d'environ 400ms avant que l'écran suivant commence à charger. (Suite) – StarDestroyer