IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Windows Presentation Foundation Discussion :

Architecture de mon algorithme: quand séparer un traitement dans un thread ?


Sujet :

Windows Presentation Foundation

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre confirmé
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Par défaut Architecture de mon algorithme: quand séparer un traitement dans un thread ?
    Salut à tous,

    Je bosse sur un algo qui itère sur des objets dans un backgroundworker nommé "Abracadabra". Je souhaite paralléliser les traitements et rendre indépendantes les parties qui ne nécessitent pas d'être synchro avec l'itération principale.

    Le nombre d'objets sur lesquels j'itère varie de, approximativement, 1000 à un million. Le gros des calculs est effectué à 2 moments:
    - Au début de la boucle
    - Toutes les 10 itérations, pas mal de tâches sont réalisées. Ça correspond à environ 3/4 des opérations totales.

    J'ai deux idées pour accélérer les choses et paralléliser les traitements:
    - Découper les objets sur lesquels j'itère en lots de x éléments et les traiter dans y instances du background worker que j'utilise. Comment déterminer la valeur idéale de x et de y, qui j'imagine est fonction des capacités du pc sur lequel j’exécute l'algo (processeur, ram)? J'ajoute que je dois conserver l'ordre des lots.
    - Lancer le traitement important qui a lieu toutes les 10 itérations dans une instance d'un autre background worker pour continuer l'itération principale (Abracadabra). La seule contrainte est de garder l'ordre des paquets de 10 qui sont traités lors de l’inscription en base en fin de traitement.

    Comment me conseillez vous de faire? Je n'aurai pas de problème si je lance cet autre backgroundworker depuis le premier (Abracadabra)? Faut-il que pour chaque paquet de 10 je lance une nouvelle instance de ce backgroundWorker secondaire ou que je fasse un truc style tableau noir: je copie au fur et à mesure les paquets de 10 itérations dans une variable publique et je les traite en parallèle avec une seule (ou plusieures, ce qui me ramène à la question précédente) instance(s) de mon backgroundworker secondaire (puis les supprime une fois traitées)? Sinon, vous connaissez une autre méthode?

    Par avance, merci de votre aide

  2. #2
    Membre Expert
    Avatar de Pragmateek
    Homme Profil pro
    Formateur expert .Net/C#
    Inscrit en
    Mars 2006
    Messages
    2 635
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 39
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Formateur expert .Net/C#
    Secteur : Conseil

    Informations forums :
    Inscription : Mars 2006
    Messages : 2 635
    Par défaut
    Sans plus de détail difficile d'être catégorique mais ça me semble un bon cas d'utilisation pour PLINQ.
    La seule contrainte étant que tu sois en .Net 4.0 ce qui n'est pas une hypothèse folle 4 ans 1/2 après sa sortie mais on ne sait jamais...

    Pour être sûr il faudrait nous montrer une version sérial de ton code.

    Et dans tous les cas multiplier les BackgroundWorkers n'est pas du tout une bonne idée.

  3. #3
    Membre Expert Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Par défaut
    Bonjour.

    * N'effectue pas un découpage rigide : ne divise pas N éléments par le nombre de processeurs en créant manuellement des threads (ou des background workers) car un processeur peut être occupé ailleurs. Passe donc par ThreadPool (1), enfile autant de lots que possible et laisse le pool créer des threads à la volée (2).

    * Tout envoi vers le ThreadPool prend quelques centaines de nanosecondes, dès lors ton unité de travail devrait être au moins de dix microsecondes pour ne pas gaspiller. Ajuste la taille de tes lots pour te situer dans cet ordre là.

    * En termes de choix des API oublie BackgroundWorker (seulement conçu pour interagir avec l'UI et plutôt obsolète même pour cet usage depuis l'arrivée des tâches et async/await). Enfile manuellement tes lots via ThreadPool.QueueUserWorkItem ou bien passe par Task.Run ou Parallel.For par exemple.

    * Si tu as besoin de faire une opération sur ton thread de départ lorsque tout le travail sera terminé, utilise Countdown ou bien Task.WhenAll.

    * Si tu as prévu de mettre à jour l'UI, attention à ce que ça ne devienne pas un point de contention.Utilise BeginInvoke plutôt que Invoke et met à jour aussi peu que possible.


    (1) Ou par Task.Run, ou TaskScheduler.Default, ou Parallel.XYZ, etc. Tous balancent le travail vers le ThreadPool.
    (2) Tu peux éventuellement aider le pool en lui spécifiant un nombre maxi de threads correspondant au nombre de processeurs si tu sais que tes threads ne seront jamais en attente d'une ressource (disque, verrou, UI, etc).

  4. #4
    Membre confirmé
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Par défaut
    Merci pour ta réponse, Pragmateek.

    Je suis en .net 4.5, donc PLINK doit être utilisable. J'ai rapidement parcouru la doc de PLINK, et je vois des éléments intéressants comme l'opérateur .AsOrdered. Par contre, le gros traitement que j'effectue toutes les 10 itérations fait environ 500 lignes de code. J'utilise habituellement LINK pour des petites tâches et j'ignore si je peux appeler mes fonctions depuis une requête LINK

    Pour être sûr il faudrait nous montrer une version sérial de ton code.
    Pas compris. Un diagramme?

  5. #5
    Membre confirmé
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Par défaut
    Merci DonQuiche.

    Ca fait pas mal d'éléments à assimiler. Je vais regarder du côté de threadpool pour voir comment ça marche.

    * Tout envoi vers le ThreadPool prend quelques centaines de nanosecondes, dès lors ton unité de travail devrait être au moins de dix microsecondes pour ne pas gaspiller. Ajuste la taille de tes lots pour te situer dans cet ordre là.
    Pas de problème de ce côté là, j'ai de nombreuses manipulations de datarow & datatables dans ma procédure à paralléliser. Le temps de moulinette serait plutôt de l'ordre des 400 ms. Pour simplifier le problème dans mon premier message, j'ai expliqué que je lançais cette procédure toutes les 10 itérations, mais en pratique, ça change à chaque fois. La fourchette classique serait plus entre 1 et 50.

    * En termes de choix des API oublie BackgroundWorker (seulement conçu pour interagir avec l'UI et plutôt obsolète même pour cet usage depuis l'arrivée des tâches et async/await). Enfile manuellement tes lots via ThreadPool.QueueUserWorkItem ou bien passe par Task.Run ou Parallel.For par exemple.
    J'avais découvert les sub/fonctions async/await en commençant ce projet (la dernière fois que j'ai fait du .net, ça remonte au 3.5) pour de la récupération de données de fichier texte. Je ne suis pas sûr de comprendre: je pensais que les fonctions async s'éxécutent dans le thread appelant. Bien qu'elles ne soient pas bloquantes pour la progression, ça ne risque pas de surcharger l'UI?

    * Si tu as prévu de mettre à jour l'UI, attention à ce que ça ne devienne pas un point de contention.Utilise BeginInvoke plutôt que Invoke et met à jour aussi peu que possible.
    Actuellement, l'UI est mise à jour depuis la sub ProgressChanged attachée au background worker à chaque fois que un paquet d'itérations est traité (de 1 à 50 comme dit au dessus). En faisant abstraction de la question du traitement par lot et par rapport au background worker, ce que tu me dis, c'est donc de mettre les tâches de mon background worker dans une sub async, et de mettre à jour ma progressbar de cette façon:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Me.Dispatcher.BeginInvoke(DispatcherPriority.Background, DirectCast(Sub() ProgressBar.Value = ProgressPercentage, ThreadStart))
    J'ai une espèce de carroussel qui tourne pendant mon itération. Il est géré à l'aide d'un timer et tous les 10 sec, il change son contenu. Pas de problème de ce côté là si je passe en async?

    merci encore

  6. #6
    Membre Expert Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Par défaut
    Citation Envoyé par billybobbonnet Voir le message
    J'avais découvert les sub/fonctions async/await en commençant ce projet (la dernière fois que j'ai fait du .net, ça remonte au 3.5) pour de la récupération de données de fichier texte. Je ne suis pas sûr de comprendre: je pensais que les fonctions async s'éxécutent dans le thread appelant. Bien qu'elles ne soient pas bloquantes pour la progression, ça ne risque pas de surcharger l'UI?
    De toute façon l'UI ne peut être manipulée que depuis le thread UI. Sinon elle lance une exception. Quand tu utilises Invoke/BeginInvoke, tu enfiles un message qui sera traité plus tard sur le thread UI, quel que soit le thread courant. Quand à BackgroundWorker.ProgressChanged, il est invoqué de la même façon en sous-main sur l'UI.

    Concernant async/await, le début de la méthode s'effectue sur le thread appelant (#1), jusqu'au premier await qui s'exécutera quant à lui sur un le thread #2 (décidé par l'expression derrière await) pendant que le thread #1 quittera ta fonction pour mener sa vie. Puis à la fin de l'expression attendue le thread #2 enverra un message dans la file d'attente de #1, qui reprendra alors ta méthode à la fin du await.

    Typiquement tu démarres une opération sur l'UI, tu attends une opération réalisée sur un autre thread, puis tu mets à jour l'UI sur le thread UI au retour du await.

    En faisant abstraction de la question du traitement par lot et par rapport au background worker, ce que tu me dis, c'est donc de mettre les tâches de mon background worker dans une sub async, et de mettre à jour ma progressbar de cette façon:
    Le code est juste. Et puisque tu n'as pas besoin d'attendre la MAJ de l'UI (tu utilises BeginInvoke au lieu de Invoke) tu n'as pas besoin d'un await, donc pas besoin non plus d'un async. Ces deux mots-clés ne servent qu'à enchaîner des continuations (l'opération faîte après un await), ce dont tu n'as pas besoin.

    J'ai une espèce de carroussel qui tourne pendant mon itération. Il est géré à l'aide d'un timer et tous les 10 sec, il change son contenu. Pas de problème de ce côté là si je passe en async?
    Aucun problème puisque BeginInvoke effectuera cette mise à jour sur le thread UI.

  7. #7
    Membre Expert
    Avatar de Pragmateek
    Homme Profil pro
    Formateur expert .Net/C#
    Inscrit en
    Mars 2006
    Messages
    2 635
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 39
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Formateur expert .Net/C#
    Secteur : Conseil

    Informations forums :
    Inscription : Mars 2006
    Messages : 2 635
    Par défaut
    Citation Envoyé par billybobbonnet Voir le message
    Par contre, le gros traitement que j'effectue toutes les 10 itérations fait environ 500 lignes de code. J'utilise habituellement LINK pour des petites tâches et j'ignore si je peux appeler mes fonctions depuis une requête LINK
    Ce n'est pas un problème, ce traitement peut être embarqué dans une méthode qui sera invoquée (via un delegate) par PLINQ.

    Citation Envoyé par billybobbonnet Voir le message
    Pas compris. Un diagramme?
    Non du code C# (ou même du pseudo-code) d'une version de l'algo non parallélisée.

  8. #8
    Membre confirmé
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Par défaut
    Pour vous permettre de mieux situer le genre de tâches que j'effectue, voilà une représentation "light" du code, avec tous les éléments d'architecture.

    Mon vrai travail commence lorsque j'ai constitué une datatable avec tous les items que je souhaite traiter. Je vous passe la déclaration et l’instanciation du background worker principal, et je commence avec sa procédure do_work.
    Code VB : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
     
     Private Sub BG_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs)
     
            Try
                Dim i As Integer = 0
     
                BG.WorkerSupportsCancellation = True
                BG.WorkerReportsProgress = True
                BG.ReportProgress(-2)
                WorkModule.LoadRessources(file)
                BG.ReportProgress(-1)
    WorkModule.startAnalysis(Txt)) 'constitution de la datatable
                For Each items As DataRow In Application.dataSt.Tables(3).Rows 'itération principale
                    WorkModule.Analyse(items , i)
                    i += 1
                    BG.ReportProgress(Math.Round(i / (items .Count / 99)))
                    If BG.CancellationPending = True Then
                        e.Cancel = True
                        Exit For
                    End If
                Next
                WorkModule.finalizeAnalysis()
                BG.ReportProgress(100)
                Exit Sub
     
            Catch ex As Exception
                MessageBox.Show(ex.ToString)
     
            End Try
     
        End Sub

    Maintenant, voilà la version light de WorkModule.Analyse(items , i), qui comme le nom l'indique est dans un module:

    Code VB : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
     
     Public Sub Analyse(ByRef row As DataRow, i As Integer)
            Try
                Dim MyTable As New DataTable
                Dim word As String = row.Item("Word")
     
                If CurrentBatchItems.Count = 0 Then 'CurrentBatchItems est une liste de datatable statique, déclaré en début de module
                    MyTable = IsItInBaseLIKE(word)
                Else
                    MyTable = IsItInBase(word)
                End If
                If MyTable Is Nothing Then 'it's not in base
                    MyTable = (UnknownIdentification(word, i)) 'we try to find candidates with additional analysis
                End If
                CurrentBatchItems.Add(MyTable ) 'we add the candidates
     
                'Est-ce le dernier du lot à traiter?
                If i < Application.dataSt.Tables(3).Rows.Count - 1 Then 'as long as it is not the last one of the table
                    Dim NextBatchID As Integer = Application.dataSt.Tables(3).Rows(i + 1)("RelatedBatchID")
                    If CurrentBatch= Nothing Then 'idem, integer statique déclaré en début de module
                        CurrentBatch= 0
                    End If
                    If CurrentBatch<> NextBatchID Then 'un nouveau lot commence à la prochaine itération
                        BigWork(CurrentBatchItems, i) 'let's process the batch
                        CurrentBatchItems.Clear()
                        CurrentBatch= NextBatchID 'update current batch ID for next loop
                    End If
                Else ' we are at the last item
                    BigWork(CurrentBatchItems, i) 
                    CurrentBatchItems.Clear()
                End If
     
                'MyTable .Clear()
            Catch ex As Exception
                MessageBox.Show(ex.ToString & ";  On : " & row.Item("Word"))
            End Try
        End Sub

    Pour ce qui est de la procédure bigWork, qui est celle qui gère les lots qui vont de 1 à 50 items environ, c'est juste de la manipulation de données qui débouche sur une inscription dans une datatable (ou table SQLite, à décider). Elle ne nécessite donc qu'une chose: que les inscriptions soient faites dans l'ordre des rows de la datatable d'origine. (et des lots qui en sont dérivés)

    En somme, pour reprendre ce que je cherche à faire, voilà l'intention en quelques points:

    - me passer du background worker si ce n'est pas une solution idéale
    - paralléliser l'itération principale sur les items de la datatable en les découpant par lots.
    - Lors du traitement de chaque datarow, je veux, lorsque le lot est terminé, lancer la tâche "BigWork" dans un autre thread et continuer le reste.

    Dites moi si je me trompe ou si ce n'est pas une bonne approche, mais voilà ce qui me vient: au sens où mes traitements, même pour un seul item, vont bien au delà des 10 microsecondes, je calibre le nombre d'items à traiter par thread sur la taille (variable) de mes lots. J'intègre dans ces mêmes threads la procédure bigwork. En gros, j'envoie tous les lots dans le threadpool, et je le laisse paralléliser idéalement le traitement des lots, et effectuer dans un même thread l'itération principale pour les items du lot et la procédure BigWork une fois les items traités. Enfin, je spécifie un nombre de threads maxi dérivé du nombre de core détectés(?).

    Si je ne m'abuse, PLINQ est une autre façon d'envoyer quelque chose dans le threadpool (dis moi si je me trompe).

  9. #9
    Membre Expert

    Homme Profil pro
    Développeur .NET
    Inscrit en
    Novembre 2010
    Messages
    2 067
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur .NET

    Informations forums :
    Inscription : Novembre 2010
    Messages : 2 067
    Par défaut
    Rien de plus simple de transformer un foreach en parrallel foreach un exemple en C# (je suis une bille en vb.net)
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    			foreach (var item in nbs)
    			{
    				total += Math.Log(item, Math.Sqrt(item));
    			}
    devient
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    			Parallel.ForEach(nbs, item =>
    			{
    				total += Math.Log(item, Math.Sqrt(item));
    			});

Discussions similaires

  1. Executer le traitement dans un thread ou BackgroundWorker
    Par skunkies dans le forum Windows Forms
    Réponses: 13
    Dernier message: 28/05/2009, 23h41
  2. Algorithme quand tu nous tiens : conditions logiques
    Par v4np13 dans le forum Algorithmes et structures de données
    Réponses: 9
    Dernier message: 21/12/2006, 19h31
  3. Probleme avec mon algorithme de tri
    Par kaygee dans le forum Langage
    Réponses: 6
    Dernier message: 09/01/2006, 21h23
  4. Problème lors de la transformation de mon "algorithm&qu
    Par prunodagen dans le forum Langage SQL
    Réponses: 8
    Dernier message: 27/04/2005, 21h48
  5. Débutant : architecture de mon site flash.
    Par Jazzy Troll dans le forum Flash
    Réponses: 3
    Dernier message: 12/01/2004, 16h36

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo