32 votes

Conseils pour la conversion d'une grande application monolithique monofilaire à une architecture multifilaire ?

Le principal produit de mon entreprise est une grande application monolithique C++, utilisée pour le traitement et la visualisation de données scientifiques. Sa base de code remonte à 12 ou 13 ans et, bien que nous ayons fait des efforts pour la mettre à jour et la maintenir (utilisation de STL et de Boost - lorsque j'ai rejoint l'entreprise, la plupart des conteneurs étaient personnalisés, par exemple - mise à niveau complète vers Unicode et la VCL 2010, etc. Comme il s'agit d'un programme de traitement et de visualisation de données, cela devient de plus en plus un handicap.

Je suis à la fois développeur y le chef de projet pour la prochaine version où nous voulons nous attaquer à ce problème, et cela va être un travail difficile dans les deux domaines. Je suis à la recherche des conseils concrets, pratiques et architecturaux sur la façon de s'attaquer au problème.

Le flux de données du programme pourrait ressembler à ceci :

  • une fenêtre doit dessiner des données
  • Dans la méthode de peinture, il appelle une méthode GetData, souvent des centaines de fois pour des centaines de bits de données en une seule opération de peinture.
  • Il va calculer ou lire un fichier ou tout ce qui est nécessaire (souvent un flux de données assez complexe - imaginez que les données circulent dans un graphe complexe, dont chaque nœud effectue des opérations).

En d'autres termes, le gestionnaire de messages de peinture se bloque pendant le traitement, et si les données n'ont pas encore été calculées et mises en cache, cela peut prendre beaucoup de temps. Parfois, il s'agit de minutes. Des chemins similaires se produisent pour d'autres parties du programme qui effectuent de longues opérations de traitement - le programme ne répond pas pendant tout ce temps, parfois pendant des heures.

Je cherche des conseils sur la manière d'aborder le changement. Des idées pratiques. Peut-être des choses comme :

  • des modèles de conception pour la demande asynchrone de données ?
  • le stockage de grandes collections d'objets de telle sorte que les threads puissent lire et écrire en toute sécurité ?
  • la gestion de l'invalidation des ensembles de données pendant que quelque chose essaie de les lire ?
  • existe-t-il des modèles et des techniques pour ce genre de problème ?
  • Qu'est-ce que je devrais demander et auquel je n'ai pas pensé ?

Je n'ai pas fait de programmation multithread depuis mes études universitaires il y a quelques années, et je pense que le reste de mon équipe est dans la même situation. Ce que je savais était académique, pas pratique, et est loin d'être suffisant pour avoir confiance dans cette approche.

L'objectif final est d'avoir un programme entièrement réactif, où tous les calculs et la génération de données sont effectués dans d'autres threads et où l'interface utilisateur est toujours réactive. Nous n'y arriverons peut-être pas en un seul cycle de développement :)


Editar: J'ai pensé que je devais ajouter quelques détails supplémentaires sur l'application :

  • Il s'agit d'une application de bureau 32 bits pour Windows. Chaque copie fait l'objet d'une licence. Nous prévoyons d'en faire une application de bureau fonctionnant en local.
  • Nous utilisons Embarcadero (anciennement Borland) C++ Builder 2010 pour le développement. Cela affecte les bibliothèques parallèles que nous pouvons utiliser, puisque la plupart semblent ( ?) être écrites uniquement pour GCC ou MSVC. Heureusement, ces derniers se développent activement et le support des standards C++ est bien meilleur qu'auparavant. Le compilateur supporte ces composants de Boost .
  • Son architecture n'est pas aussi propre qu'elle devrait l'être et les composants sont souvent trop étroitement couplés. C'est un autre problème :)

Edit #2 : Merci pour les réponses jusqu'à présent !

  • Je suis surpris qu'autant de personnes aient recommandé une architecture multiprocessus (c'est la réponse la plus votée pour le moment), et non le multithreading. J'ai l'impression que c'est une structure de programme très Unix, et je ne sais rien sur la façon dont elle est conçue ou fonctionne. Y a-t-il de bonnes ressources disponibles à ce sujet, sous Windows ? Est-ce vraiment si courant sous Windows ?
  • En termes d'approches concrètes de certaines des suggestions de multithreading, existe-t-il des modèles de conception pour la demande et la consommation asynchrones de données, ou des systèmes MVP asynchrones ou threadaware, ou comment concevoir un système orienté tâche, ou des articles, des livres et des déconstructions après publication illustrant les choses qui fonctionnent et celles qui ne fonctionnent pas ? Nous pouvons développer toute cette architecture nous-mêmes, bien sûr, mais il est bon de travailler à partir de ce que d'autres ont fait auparavant et de connaître les erreurs et les pièges à éviter.
  • Un aspect qui n'est pas abordé dans les réponses est la gestion de projet. J'ai l'impression qu'il est difficile d'estimer le temps que cela prendra et de garder un bon contrôle du projet lorsque l'on fait quelque chose d'aussi incertain que cela. C'est l'une des raisons pour lesquelles je recherche des recettes ou des conseils pratiques en matière de codage, je suppose, pour guider et limiter autant que possible la direction du codage.

Je n'ai pas encore marqué de réponse à cette question - ce n'est pas à cause de la qualité des réponses, qui est excellente (et merci), mais simplement parce qu'en raison de la portée de cette question, j'espère plus de réponses ou de discussions. Merci à ceux qui ont déjà répondu !

16voto

John Dibling Points 56814

Vous avez un grand défi à relever. J'avais un défi similaire à relever : une base de code monolithique monofilaire vieille de 15 ans, ne tirant pas parti du multicœur, etc. Nous avons déployé beaucoup d'efforts pour essayer de trouver une conception et une solution qui soit réalisable et qui fonctionne.

La mauvaise nouvelle d'abord. Il sera à la fois peu pratique et impossible de rendre votre application monofilaire multi-filière. Une application monofilaire repose sur son caractère monofilaire de manière à la fois subtile et grossière. Par exemple, si la partie calcul requiert des données de la partie interface graphique. L'interface graphique doit fonctionner dans le thread principal. Si vous essayez d'obtenir ces données directement du moteur de calcul, vous vous heurterez probablement à des situations de blocage et de course qui nécessiteront une refonte majeure pour les résoudre. La plupart de ces problèmes n'apparaîtront pas pendant la phase de conception, ni même pendant la phase de développement, mais seulement après avoir soumis une version à un environnement difficile.

Encore des mauvaises nouvelles. La programmation d'applications multithread est exceptionnellement difficile. Il peut sembler assez simple de verrouiller des éléments et de faire ce que l'on a à faire, mais ce n'est pas le cas. Tout d'abord, si vous verrouillez tout ce que vous voyez, vous finissez par sérialiser votre application, ce qui annule tous les avantages du mutithreading et ajoute encore à la complexité. Même si vous dépassez ce stade, écrire une application MP sans défaut est déjà difficile, mais écrire une application MP hautement performante est encore plus difficile. Vous pouvez apprendre sur le tas, dans une sorte de baptême du feu. Mais si vous faites cela avec du code de production, en particulier héritage le code de production, vous mettez votre entreprise en danger.

Maintenant les bonnes nouvelles. Il existe des options qui n'impliquent pas de remanier l'ensemble de votre application et qui vous donneront la plupart de ce que vous recherchez. Une option en particulier est facile à mettre en œuvre (en termes relatifs), et beaucoup moins sujette à des défauts que de rendre votre application entièrement MP.

Vous pourriez instancier plusieurs copies de votre application. Rendez l'une d'entre elles visible, et toutes les autres invisibles. Utilisez l'application visible comme couche de présentation, mais n'y effectuez pas le travail de calcul. Au lieu de cela, envoyez des messages (peut-être via des sockets) aux copies invisibles de votre application qui effectuent le travail et renvoient les résultats à la couche de présentation.

Cela peut sembler être un piratage. Et c'est peut-être le cas. Mais il vous permettra d'obtenir ce dont vous avez besoin sans mettre la stabilité et les performances de votre système en danger. De plus, il y a des avantages cachés. L'un d'entre eux est que les copies invisibles du moteur de votre application auront accès à leur propre espace mémoire virtuel, ce qui facilitera l'exploitation de l'espace mémoire virtuel. todo les ressources du système. Il est également très facile à mettre à l'échelle. Si vous utilisez une boîte à 2 cœurs, vous pouvez faire tourner 2 copies de votre moteur. 32 cœurs ? 32 copies. Vous voyez l'idée.

15voto

Andrew McGregor Points 7641

Il y a donc un indice dans votre description de l'algorithme sur la façon de procéder :

souvent un flux de données assez complexe - pensez-y comme des données circulant dans un graphe complexe, dont chaque nœud effectue des opérations

Je chercherais à faire en sorte que ce graphique de flux de données soit littéralement la structure qui fait le travail. Les liens dans le graphe peuvent être des files d'attente à sécurité thread, les algorithmes à chaque nœud peuvent rester pratiquement inchangés, sauf qu'ils sont enveloppés dans un thread qui prend les éléments de travail dans une file d'attente et dépose les résultats dans l'une d'elles. Vous pouvez aller un peu plus loin et utiliser des sockets et des processus plutôt que des files d'attente et des threads ; cela vous permettra de vous répartir sur plusieurs machines si cela présente un avantage en termes de performances.

Ensuite, vos méthodes de peinture et autres interfaces graphiques doivent être divisées en deux : une moitié pour mettre le travail en file d'attente, et l'autre moitié pour dessiner ou utiliser les résultats lorsqu'ils sortent du pipeline.

Cela peut ne pas être pratique si l'application suppose que les données sont globales. Mais si elles sont bien contenues dans des classes, comme votre description le suggère, cela pourrait être le moyen le plus simple de les mettre en parallèle.

8voto

dthorpe Points 23314
  1. N'essayez pas de tout multithreader dans l'ancienne application. Multithreader pour le plaisir de dire que c'est multithreadé est une perte de temps et d'argent. Vous construisez une application qui fait quelque chose, pas un monument à vous-même.
  2. Établissez le profil de vos flux d'exécution et étudiez-les pour savoir où l'application passe le plus de temps. Un profileur est un excellent outil pour cela, mais le simple fait de parcourir le code dans le débogueur l'est tout autant. C'est en marchant au hasard que l'on trouve les choses les plus intéressantes.
  3. Découplage de l'interface utilisateur et des calculs de longue durée. Utilisez des techniques de communication inter-filières pour envoyer des mises à jour à l'interface utilisateur à partir du fil de calcul.
  4. Comme effet secondaire du point 3, réfléchissez bien à la réentrance : maintenant que le calcul s'exécute en arrière-plan et que l'utilisateur peut se balader dans l'interface utilisateur, quelles sont les choses à désactiver dans l'interface utilisateur pour éviter les conflits avec l'opération en arrière-plan ? Permettre à l'utilisateur de supprimer un ensemble de données alors qu'un calcul est en cours d'exécution sur ces données est probablement une mauvaise idée. (Atténuation : le calcul fait un instantané local des données) Est-il logique pour l'utilisateur de lancer plusieurs opérations de calcul simultanément ? S'il est bien géré, cela pourrait être une nouvelle fonctionnalité et aider à rationaliser l'effort de refonte de l'application. Si elle est ignorée, ce sera un désastre.
  5. Identifier les opérations spécifiques qui sont candidates pour être poussées dans un fil de fond. Le candidat idéal est généralement une fonction ou une classe unique qui effectue un travail important (nécessitant beaucoup de temps - plus de quelques secondes) avec des entrées et des sorties bien définies, qui n'utilise aucune ressource globale et qui ne touche pas directement l'interface utilisateur. Évaluez et hiérarchisez les candidats en fonction de la quantité de travail nécessaire pour passer à cet idéal.
  6. En termes de gestion de projet, il faut faire les choses étape par étape. Si vous avez plusieurs opérations qui sont de bons candidats pour être déplacées vers un thread d'arrière-plan, et qu'elles n'ont aucune interaction entre elles, elles peuvent être implémentées en parallèle par plusieurs développeurs. Cependant, il serait bon que tout le monde participe d'abord à une conversion afin que chacun comprenne ce qu'il faut rechercher et que vous puissiez établir vos modèles d'interaction avec l'interface utilisateur, etc. Organisez une réunion élargie au tableau blanc pour discuter de la conception et du processus d'extraction de la fonction unique dans un thread d'arrière-plan. Mettez le tout en œuvre (ensemble ou individuellement), puis réunissez-vous à nouveau pour mettre tout cela en commun et discuter des découvertes et des difficultés rencontrées.
  7. Le multithreading est un casse-tête et nécessite une réflexion plus approfondie que le codage pur et simple, mais la division de l'application en plusieurs processus crée bien plus de maux de tête, selon moi. Le support du threading et les primitives disponibles sont bons sous Windows, peut-être meilleurs que sur certaines autres plateformes. Utilisez-les.
  8. En général, ne faites pas plus que ce qui est nécessaire. Il est facile de surimposer et de compliquer un problème en y ajoutant des modèles et des bibliothèques standard.
  9. Si aucun membre de votre équipe n'a jamais travaillé sur le multithreading, prévoyez du temps pour former un expert ou des fonds pour en engager un en tant que consultant.

7voto

John Knoeller Points 20754

La principale chose que vous devez faire est de déconnecter votre interface utilisateur de votre ensemble de données. Je suggère que la façon de le faire est de mettre une couche entre les deux.

Vous devrez concevoir une structure de données de données cuites pour l'affichage. Cette structure contiendra très probablement des copies de certaines de vos données dorsales, mais "cuites" pour être faciles à dessiner. L'idée principale est de pouvoir peindre rapidement et facilement à partir de ces données. Vous pouvez même faire en sorte que cette structure de données contienne des positions calculées à l'écran de bits de données afin de pouvoir dessiner rapidement.

Chaque fois que vous obtenez un message WM_PAINT, vous devez récupérer le plus récent complet version de cette structure et s'en inspirer. Si vous faites cela correctement, vous devriez être en mesure de gérer plusieurs messages WM_PAINT par seconde, car le code de peinture ne fait jamais référence à vos données de base. Il ne fait que parcourir la structure cuite. L'idée ici est qu'il est préférable de peindre rapidement des données périmées plutôt que de bloquer votre interface utilisateur.

En attendant...

Vous devriez avoir 2 copies complètes de cette structure cuite pour l'affichage. L'une est ce que le message WM_PAINT regarde. (appelez-la cfd_A ) L'autre est ce que vous donnez à votre fonction CookDataForDisplay(). (appelez-la cfd_B ). Votre fonction CookDataForDisplay() s'exécute dans un thread séparé, et travaille sur la construction/la mise à jour de cfd_B en arrière-plan. Cette fonction peut prendre autant de temps qu'elle le souhaite car elle n'interagit en aucune façon avec l'écran. Une fois l'appel retourné cfd_B sera la version la plus à jour de la structure.

Échangez maintenant cfd_A y cfd_B et InvalidateRect sur la fenêtre de votre application.

Une façon simpliste de procéder est de faire en sorte que votre structure d'affichage soit un bitmap, et c'est peut-être une bonne façon de commencer à travailler, mais je suis sûr qu'avec un peu de réflexion, vous pouvez faire un bien meilleur travail avec une structure plus sophistiquée.

Donc, pour en revenir à votre exemple.

  • Dans la méthode de peinture, il appelle une méthode GetData, souvent des centaines de fois pour des centaines de bits de données en une seule opération de peinture.

Il s'agit maintenant de 2 threads, la méthode de peinture fait référence à cfd_A et s'exécute sur le thread de l'interface utilisateur. Pendant ce temps, cfd_B est construit par un thread d'arrière-plan en utilisant des appels GetData.

Le moyen le plus rapide de le faire est de

  1. Prenez votre code WM_PAINT actuel, collez-le dans une fonction appelée PaintIntoBitmap().
  2. Créer un bitmap et un DC de mémoire, c'est cfd_B.
  3. Créez un thread, passez-lui cfd_B et demandez-lui d'appeler PaintIntoBitmap().
  4. Quand ce fil est terminé, échangez cfd_B et cfd_A.

Maintenant, votre nouvelle méthode WM_PAINT prend simplement le bitmap pré-rendu dans cfd_A et le dessine à l'écran. Votre interface utilisateur est maintenant déconnectée de la fonction GetData() de votre backend.

C'est maintenant que le vrai travail commence, car la méthode rapide ne gère pas très bien le redimensionnement des fenêtres. Vous pouvez ensuite affiner les structures cfd_A et cfd_B petit à petit jusqu'à ce que vous soyez satisfait du résultat.

6voto

Byron Whitlock Points 29863

Vous pourriez commencer par diviser l'interface utilisateur et la tâche de travail en fils séparés.

Dans votre méthode de peinture, au lieu d'appeler directement getData(), vous placez la requête dans une file d'attente à sécurité thread. getData() est exécuté dans un autre thread qui lit ses données dans la file d'attente. Lorsque le thread getData a terminé, il signale au thread principal de redessiner la zone de visualisation avec ses données de résultat en utilisant la synchronisation des threads pour transmettre les données.

Pendant que tout cela se passe, vous avez bien sûr une barre de progression indiquant les cannelures de réticulation pour que l'utilisateur sache que quelque chose se passe.

Cela permettrait de conserver une interface utilisateur rapide sans avoir à subir les inconvénients du multithreading de vos routines de travail (qui peut s'apparenter à une réécriture totale).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X