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

  1. #1
    Membre du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    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
    Expert confirmé
    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 : 37
    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
    Points : 4 062
    Points
    4 062
    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.
    Formateur expert .Net/C#/WPF/EF Certifié MCP disponible sur Paris, province et pays limitrophes (enseignement en français uniquement).
    Mon blog : pragmateek.com

  3. #3
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    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 du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    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 du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    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
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    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
    Expert confirmé
    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 : 37
    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
    Points : 4 062
    Points
    4 062
    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.
    Formateur expert .Net/C#/WPF/EF Certifié MCP disponible sur Paris, province et pays limitrophes (enseignement en français uniquement).
    Mon blog : pragmateek.com

  8. #8
    Membre du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    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
    Expert confirmé

    Homme Profil pro
    Développeur .NET
    Inscrit en
    Novembre 2010
    Messages
    2 065
    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 065
    Points : 4 229
    Points
    4 229
    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));
    			});

  10. #10
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    @billybobbonnet
    J'ai deux problèmes avec ta proposition :

    a) Le fait que tes tâches consistent à insérer des lignes dans la DB change pas mal la donne et me fait dire que celle-ci ou le disque dur risquent d'être le goulet d'étrangement. Surtout si tu utilises SQLite. Malheureusement j'utilise trop peu les DB pour te conseiller et de toute façon cela dépendrait de ton choix de SGBD et du risque de contention associée à ton schéma et à tes traitements, mais disons que tu devras rapidement faire des mesures pour vérifier que ton gain est bien réel et il se pourrait que tu doives procéder à des changements pour batcher (isoler les traitements de l'envoi à la DB, et opérer celle-ci via SQLBulkCopy) ou alléger la contention. Il va falloir expérimenter.

    b) Tu dis que tes lignes doivent être insérées dans un certain ordre. Cet ordre est-il local, lié au seul lot et géré par un seul thread, ou global dépendant de la même table source et requérant une coopération entre les threads? Parce que tes lots, eux, ne vont pas forcément s'être traités dans l'ordre dans lequel tu les auras planifiés : un thread pourrait être mis en pause pour libérer un coeur, prenant ainsi du retard sur les autres. Si tu dois imposer un ordre alors tu vas devoir mettre en place une synchronisation qui limitera les performances, voire avoir recours à un motif producteur/consommateur.

    A part ça, gaffe à l'incrémentation de la progression ou à d'autres éventuels états partagés, voir ci-dessous. Je ne suis même pas sûr que DataSet soit partageable en lecture.


    @youtpout978
    En fait ton code est incorrect et ne serait pas efficace.

    Il est incorrect parce que si deux threads exécutent "charger x, ajouter 1 à x, stocker nouvelle valeur de x", alors l'ordre final d’exécution pourrait être: charger x dans x1, charger x dans x2, ajouter 1 à x1, ajouter 1 à x2, stocker x1 dans x, stocker x2 dans x. Si bien x + 1 + 1 donne parfois x + 1. D'où l'intérêt des verrous et de Inrerlocked.Increment.

    Quant à l'efficacité, c'était justement le but de la question de Billy. Ici tu passerais 500ns à enfiler une opération qui prendra 1ns.

  11. #11
    Membre du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    Par défaut
    - Apparemment, il n'est pas possible (ou recommandé) d'utiliser une parallélisation de tâches modifiant une datatable vu qu'elle n'est pas thread-safe et ne tolère pas les accès concurrents. J'en déduis que soit je dois passer par des tables intermédiaires, soit je passe par des inserts dans une DB SQLite (ce qui sera probablement mon choix).

    @youtpout978 - Même pour consultation, on ne peut pas calquer ton exemple de Parallel.ForEach sur le cas d'une collection de datarow appartenant à une datatable car elle est IEnumerable mais pas IEnumerable<Datarow> En C#, il suffit de rajouter à la collection .AsEnumerable() comme ci-dessous:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
     
    Parallel.ForEach(dt.AsEnumerable(), drow =>
    {
        ...
        Faire qqch
        ...
    });
    par contre, je n'arrive pas à traduire ce code en VB, y compris avec l'aide des outils de conversion de code.

    - La méthode Parallel.ForEach ne maintient pas l'ordre des éléments de la collection, du coup, ça semble compromis

    J'ai beaucoup de pistes pour résoudre ce problème, voire trop. Du coup je suis un peu perdu. Je ne connais pas bien les options à disposition pour du multithread et j'ai du mal à trouver parmi les solutions proposées la meilleure. Par exemple, vu que mes lots sont de taille variable, j'imagine que la fin de leur thread (et donc l'inscription en base) risque de survenir dans le désordre.

    Autre question, si je traite en parallèle mes lots au lieu d'itérer dessus, je me demande si c'est encore pertinent d'isoler et de rendre indépendante la tâche BigWork qui est appelée une fois en fin de chaque lot: mieux vaut séquentialiser l'ensemble des opérations au sein de chaque lot, non?

    Dites moi si je peux préciser quoique ce soit pour vous aider à m'orienter!

  12. #12
    Membre du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    Par défaut
    DonQuiche,

    a) Tu as raison, et l'histoire se répète un peu. J'avais au départ un insert en DB SQLite par itération. Rapidement, je me suis aperçu que ça ralenti méchamment l'ensemble et que ce n'est pas l'idéal. J'ai donc opté pour un stockage intermédiaire en datatable et un insert de l'ensemble dans la DB en fin de traitement, ce qui me permettait de les faire au sein de la même transaction (incomparablement plus rapide). Au fur et à mesure que j'ai rajouté des opérations dans mon itération, elle est devenue plus longue (jusqu'ici tout est normal ). Du coup, j'ai décidé de poster ce sujet pour paralléliser la boucle. Or, pour paralléliser l'itération, je dois abandonner ma datatable à cause des accès concurrents, et je me retrouve de nouveau confronté au problème des inserts DB à chaque boucle.

    b) Je dois clarifier les choses concernant l'ordre. L'ensemble des données doit rester ordonné. Chaque lot doit être stocké à sa place et au sein de chaque lot, chaque item doit rester à sa place. D'un point de vue général, ce que fait mon algo actuel, c'est prendre une database, itérer sur chaque ligne et pour chacune d'entre elles (sauf rare exception) créer une ligne dans une autre database dans le même ordre. J'ai un index numérique dans la datatable aussi bien pour les lots que pour les items. Je n'envisageais pas de devoir reclasser l'ensemble en fin de traitement mais ça pourrait être une option.

    La solution qui m'apparaît la plus simple pour le moment c'est de récupérer, lorsque chaque thread se termine, une datatable intermédiaire avec les items de mon lot. Je fusionne alors cette datatable avec celle des résultats, et en fin de traitement je classe mes résultats en utilisant l'index de chaque lot. De cette façon, peu importe l'ordre de traitement.

  13. #13
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par billybobbonnet Voir le message
    - Apparemment, il n'est pas possible (ou recommandé) d'utiliser une parallélisation de tâches modifiant une datatable vu qu'elle n'est pas thread-safe et ne tolère pas les accès concurrents. J'en déduis que soit je dois passer par des tables intermédiaires, soit je passe par des inserts dans une DB SQLite (ce qui sera probablement mon choix).
    Attention :
    * Il va de soi que le type DataTable n'est pas thread-safe en écriture, mais rien n'interdit a priori d'utiliser une instance par thread, représentant toutes la même table, avec une transaction par thread.
    * DataTable est vraisemblablement thread-safe en lecture.
    * Un SGBD est conçu pour supporter efficacement les accès concurrents. Le SGBD et le type dotnet DataTable sont deux choses totalement différentes !
    * Conçu pour supporter les accès concurrents ne veut pas dire faire de miracles : même si les accès disques sont optimisés, celui-ci a de bonnes chances d'être rapidement un goulet d'étranglement. Et si ce n'est pas lui ce sera sans doute la DB.


    La méthode Parallel.ForEach ne maintient pas l'ordre des éléments de la collection, du coup, ça semble compromis
    On en revient à ce que je disais : l'ordre de traitement des lots ne sera pas l'ordre dans lequel tu les auras spécifié parce qu'à tout moment l'OS peut retirer un coeur à ton application pour le consacrer à autre chose. Si tu veux imposer un ordre global lors de l'écriture, tu vas devoir synchroniser, par exemple via :

    Code c# : 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
    static Object NextWriterLock= new Object();
    static int NextWriter;
     
    void BigWork(int order)
    {
        PrepareWrites();
     
        lock(NextWriterLock) 
        {
            while (NextWriter != order) 
                Monitor.Wait(NextWriterLock); // Attend un NextWriter++
     
            Write();
     
            NextWriter++;
            Monitor.PulseAll(NextWriterLock); // Réveille ceux en attente de NextWriter++
        }
    }

    Le problème évidemment c'est que cela empêche l'exécution parallèle du bloc d'écriture. Un seul thread pourra le faire à la fois. Paralléliser n'a plus d'intérêt que si la préparation des écritures est longue.


    Autre question, si je traite en parallèle mes lots au lieu d'itérer dessus, je me demande si c'est encore pertinent d'isoler et de rendre indépendante la tâche BigWork qui est appelée une fois en fin de chaque lot: mieux vaut séquentialiser l'ensemble des opérations au sein de chaque lot, non?
    A priori il semble effectivement préférable de traiter un lot par thread.

    Mais j'insiste sur le fait que pour l'heure le plus urgent est de tester si les opérations peuvent être efficacement parallélisées. Car si le disque dur ou la DB sont le goulet d'étranglement actuel, répartir le travail sur plusieurs CPU n'apportera rien. Cela n'a d'intérêt que si le CPU est aujourd'hui le facteur limitant. Efforce-toi de faire un test rapide pour le savoir.

    Edit : confusion sur SQLite.

  14. #14
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par billybobbonnet Voir le message
    b) Je dois clarifier les choses concernant l'ordre. L'ensemble des données doit rester ordonné. Chaque lot doit être stocké à sa place et au sein de chaque lot, chaque item doit rester à sa place. D'un point de vue général, ce que fait mon algo actuel, c'est prendre une database, itérer sur chaque ligne et pour chacune d'entre elles (sauf rare exception) créer une ligne dans une autre database dans le même ordre.
    Et tu ne peux pas recourir à une procédure stockée ou une requête ? Ca a de grandes chances d'être plus efficace.
    Maintenant, comme je l'ai souligné, je connais trop peu les bases de données pour pouvoir te diriger avec certitude sur la meilleure approche.

  15. #15
    Membre du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    Par défaut
    Et tu ne peux pas recourir à une procédure stockée ou une requête ? Ca a de grandes chances d'être plus efficace.
    Non, les transformations sont trop complexes et nombreuses. Mes données, entre la table d'origine et la finale, intègrent des infos de plusieurs autres tables et transforment lourdement l'ensemble.

    Je vais faire un peu d'essai-erreur pour voir où sont les murs. Avec une légère modification de ma boucle pour qu'elle traite des lots plutôt que des items, j'arrive à lancer ma procédure pour chaque lot comme ça:
    Code VB : Sélectionner tout - Visualiser dans une fenêtre à part
    ThreadPool.QueueUserWorkItem(Sub() WorkModule.Analyse(MonLot)))

    qui donne, je crois, en c#
    Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
    ThreadPool.QueueUserWorkItem(() => WorkModule.Analyse(MonLot))

    Évidemment, le stockage de la donnée en fin de thread ne marchera pas. Il me reste à trouver comment récupérer une datatable intermédiaire en sortie afin gérer la mise en commun des données en fin de traitement.

  16. #16
    Expert confirmé

    Homme Profil pro
    Développeur .NET
    Inscrit en
    Novembre 2010
    Messages
    2 065
    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 065
    Points : 4 229
    Points
    4 229
    Par défaut
    C'était juste un exemple mon Parallel foreach, je n'ai pas vraiment regardé son code.

    Il serait intéressant de savoir ce qu'il cherche à faire, après rien ne t'obliges d'utiliser une datatable tu peux utiliser une collection typé, et même mettre en œuvre des mécanismes te permettant de remettre en ordre tes données.

    Quel est ton but de ton traitement ?
    Après comme dit DonQuiche une mesure des performances peut être intéressant pour connaitre le véritable goulot d'étranglement, si tu n'as pas de fonctionnalité particulière pour faire ça il existe la class Stopwatch pour le faire.

  17. #17
    Membre du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    Par défaut
    J'observe de près chacune des parties de mon algo depuis que je l'ai commencé. Je sais d'où viennent les latences et je pense qu'elles sont uniquement liées au processeur:
    - Je n'écris et ne lis rien sur le disque avant la fin de l'algo
    - La consommation maxi de RAM monte à 58 Mo.

    Sauf autre étranglement possible dont je n'ai pas connaissance, ça devrait être le proc.

    Si le traitement est trop long, c'est surtout qu'il fait beaucoup de choses. Les petits ruisseaux font les grandes rivières Disons qu'il est certainement améliorable, mais je pense que c'est principalement de l'ajustement en finesse.

    L'essentiel des tâches tombe dans 3 catégories:
    - manipulation de string & regex
    - la lecture/écriture de datarow d'après l'index (les datatables et datarow du lot se codéfinissent)
    - recherche en DB via index qui va chercher dans les 7ms/requête.

    Petite question au passage:
    mes regex sont en mode "compiled". Ils appartiennent à un module (donc statiques). Je crois qu'ils sont compilés lors du premier appel au module. Or, ce module est utilisé au sein de la procédure d'analyse, laquelle sera appelée dans les threads des lots. Est-ce que ça veut dire que chaque lot lancera une compilation du regex ou que seul le premier le fera?

    J'ai envisagé d'essayer de mettre des listes de listes de tableaux de string à la place de datatables mais je ne sais pas si j'y gagnerais en perf, et surtout ça serait très pénible à l'usage (impossible de désigner un champ par son nom de colonne).

    Je retourne à mon code pour pondre une première version parallélisée comme il suit:
    - For each lot, j'appelle un ThreadPool.QueueUserWorkItem
    - Dans chaque lot, je commence par traiter tous les items avec un parallel.for (qui je crois garde l'ordre des éléments) et je finis par big work
    - A l'issue de bigwork, j'inscris en base SQLite tous les items. A priori, si j'utilise une connexion/transaction SQLite unique pour chaque lot, ça devrait être toléré. En tout cas, je vais tenter.

    Je vous tiens au courant des progrès.

    Merci encore à tous pour votre aide!

  18. #18
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par billybobbonnet Voir le message
    L'essentiel des tâches tombe dans 3 catégories:
    - manipulation de string & regex
    - la lecture/écriture de datarow d'après l'index (les datatables et datarow du lot se codéfinissent)
    - recherche en DB via index qui va chercher dans les 7ms/requête.
    Dans ce cas ton code devrait être très parallélisable. Même les requêtes en lecture devraient pouvoir être efficacement concurrentes avec beaucoup moins de risque que le disque constitue un goulet (a priori toutes les données seront dans le cache).

    Est-ce que ça veut dire que chaque lot lancera une compilation du regex ou que seul le premier le fera?
    En principe seul le premier, à moins qu'ils aient codé ça avec les pieds. De toute façon c'est un détail vu la durée de test traitements.

    J'ai envisagé d'essayer de mettre des listes de listes de tableaux de string à la place de datatables mais je ne sais pas si j'y gagnerais en perf, et surtout ça serait très pénible à l'usage (impossible de désigner un champ par son nom de colonne).
    Encore une fois je connais trop peu DataTable et DataRow mais il me semble que :
    * Toute modification d'une ligne réclame un bazar monstre.
    * Le contenu d'une ligne est en fait stocké dans DataTable.
    * Toute ligne ajoutée à une DataTable ne peut pas ensuite être ajoutée à une autre DataTable.

    Du coup il semble plutôt coûteux de préparer et transférer des datarow qui devront de toute façon être recopiés.

  19. #19
    Membre du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    Par défaut
    Quelques nouvelles:

    - J'ai minuté au sein d'un lot la différence entre un for et un parallel.for, et le résultat est surprenant: environ 30% plus lent avec le parallel for. Le souci expliqué ci dessous pourrait être la cause de ce ralentissement.
    - J'ai ajouté en fin de traitement de lot une routine d'insert SQLite des données pour remplacer la mise en datatable afin de de paralléliser le traitement des lots comme ça:

    Code VB : Sélectionner tout - Visualiser dans une fenêtre à part
    ThreadPool.QueueUserWorkItem(Sub() WorkModule.Analyse(MonLot)))

    Je suis étonné de voir que les threads semblent partager les variables des fonctions qu'ils appellent. En gros, quand je traite un lot (WorkModule.Analyse(MonLot)) je fais appel à des fonctions statiques qui sont dans WorkModule et d'autres modules. Or, elles semblent ne pas être exclusives à chaque thread, mais plutôt partagées. Il en suit des erreurs évidentes.

    Pardonnez la naïveté de la question, c'est la première fois que je me frotte à ce genre de choses. Quelle est la solution?

    -embarquer tout le code qui est hors de WorkModule.Analyse à l'intérieur, ce qui me ferait une énorme sub
    - créer un objet Workmodule qui est instancié autant de fois que nécessaire (est-ce que les appels de cet objet à des fonctions statiques, dans les modules, poseront toujours problème?)
    - Embarquer dans l'objet Workmodule toutes les fonctions appelées, y compris les routines des inserts SQLite (voir la question du point précédent)?
    - J'ajoute l'ID du lot en argument de chacune des fonctions appelées et je crée une version de chaque variable selon l'ID (version compliquée je suppose)

  20. #20
    Membre du Club
    Inscrit en
    Mars 2009
    Messages
    104
    Détails du profil
    Informations forums :
    Inscription : Mars 2009
    Messages : 104
    Points : 69
    Points
    69
    Par défaut
    Au passage, DonQuiche, vu que j'ai l'occasion de partager le peu que je sais:

    * Toute ligne ajoutée à une DataTable ne peut pas ensuite être ajoutée à une autre DataTable.
    C'est possible à l'aide de la fonction datatable.ImportRow à condition que le schéma soit compatible, ce qui est faisable en faisant comme il suit:

    Code VB : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    Dim NouvelleTable as new Datatable
    NouvelleTable = AncienneTable.clone 'Copier le schéma
     
    For each row as Datarow in AncienneTable
           NouvelleTable.ImportRow(row) 'Importer la row
    Next

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