Structure efficace pour l’access multi-thread

J’ai besoin d’implémenter un mécanisme qui a une datastruture (queue en ce moment) qui contient une liste d’objects de requête en attente qui sont marqués par différents threads lorsqu’ils sont utilisés et retirés lorsqu’un thread a fini de l’utiliser.

Cette structure de données peut contenir jusqu’à quelques milliers d’éléments à tout moment. N threads recevront les requêtes de celui-ci (le marquant essentiellement comme étant ‘repris’). retirez-le.

Je me demandais maintenant à quel point une file d’attente STL C ++ serait efficace et qu’il faudrait chercher à nouveau le même élément pour le supprimer de la file d’attente.

Je ne veux pas que cette structure de données soit verrouillée par un mécanisme de synchronisation de threads trop longtemps, car un thread recherche un élément quelque part dans celui-ci. Cela pourrait enfermer tout mon programme. (Le programme doit être très performant et rapide)

Quelqu’un peut-il donner des conseils sur la meilleure façon de mettre en œuvre cela dans un environnement multithreading afin que la structure ne soit pas verrouillée trop longtemps lorsqu’une recherche doit être effectuée?

Vous pouvez vous concentrer sur ce qui n’est pas la partie la plus difficile de votre conception ici.

Si la queue est FIFO sans aucune priorité, vos accesseurs vont être push_back () et pop_front () – très rapidement, même si vous n’avez pas la peine d’utiliser la sémantique de comparaison-et-swap (CAS) mais respectez le mutex simple / section critique. Si vous avez besoin de pouvoir hiérarchiser le trafic, les choses se compliquent. Si vous utilisez le locking CAS alors (sous Windows de toute façon) vous ne pouvez pas améliorer le shared_mutex de boost :: thread sans passer beaucoup trop de temps à faire cette partie de votre codage. Pas sûr des implémentations non-Windows.

La partie la plus complexe de ce problème est généralement de signaler aux threads de travail inactifs de prendre un nouveau travail. Vous ne pouvez pas les laisser en boucle tant que queue.front () n’est pas vide, vous devez donc vous assurer que le nombre correct de threads inactifs reçoit le coup d’envoi pour récupérer les éléments en queue. Lorsqu’un thread de travail devient inactif, il peut rechercher un nouveau travail et l’exécuter si tel est le cas. Dans le cas contraire, l’état de la queue doit être défini sur inactif afin que le prochain push_back entraîne un “réveil” afin de redémarrer le pool de threads de travail. Cette zone doit être robuste à 100% contre toutes les exceptions non fatales, sinon votre processus deviendra sombre.

Êtes-vous en train de gérer vos propres threads ou d’utiliser un pool de threads intégré? Envisagez-vous de disposer d’un pool de threads de taille dynamic ou simplement de générer N threads (configurables vraisemblablement) et de les faire fonctionner jusqu’à la fin du processus?

Demander aux threads de travailler de se connecter au processus d’élément de travail. Comprendre à qui appartient un élément de travail à n’importe quelle partie de son cycle de vie est vital. Arrêtez / démarrez le travail, et un résumé de la tâche et du temps sera utile. si la journalisation est lente, transmettez-la à un thread séparé via une queue fire-and-oublier, mais vous devez alors rechercher la latence, ce qui rend votre journal moins utile. Si vous avez besoin de la capacité de manipuler en externe des travaux en cours, une structure distincte de votre queue de travaux en attente – des éléments de travail en cours indexés par thread et affichant l’état actuel / l’heure de début, avec un locking séparé, semble être une bonne idée. Cette structure sera O (nombre de threads), donc plus petite que la queue “en attente”, elle ne risque donc pas de constituer un goulot d’étranglement si les opérations résultantes à long terme sont exécutées en dehors du verrou de structure.

Concernant les performances, que vont faire vos threads de travail? Si les éléments de travail doivent durer longtemps, faire beaucoup d’E / S ou d’autres opérations coûteuses, alors l’interaction de queue n’est pas votre goulot d’étranglement de performances, aussi une sur-optimisation de cette zone est relativement improductive. Envisagez la perfor- mance de l’ensemble du système dans votre conception, et pas seulement d’une petite zone.

Ceci est juste pour les débutants. Bonne chance, ce n’est pas un système facile à concevoir de manière robuste.

[EDIT] basé sur la description du poste de travail.

L’parsing devrait être rapide (bien que cela puisse impliquer une récupération coûteuse des données source – difficile à dire?), L’access aux bases de données moins. On dirait que le réglage de la firebase database peut être votre meilleur rapport qualité-prix. Si vous n’en avez pas le contrôle, il vous suffit d’atténuer autant que possible les bases de données lentes dans votre conception. Si vous avez la possibilité de créer un access asynchrone à une firebase database, le thread de travail peut simplement effectuer suffisamment de travail pour lancer l’appel de firebase database, puis terminer le travail sur un rappel, permettant ainsi à un autre travail de démarrer sur le thread de travail. Sans access à la firebase database asynchrone, il sera difficile d’implémenter un délai de requête fiable sans une autre méthode d’indirection lorsque le thread de travail principal n’attend pas que les appels de firebase database soient terminés en ligne. Vous devez dissocier vos threads de travail principaux de la dépendance à la firebase database, à moins que vous ne puissiez faire confiance à la firebase database pour qu’elle retourne ou commette une erreur en temps voulu. Peut-être un délai d’expiration configurable ou spécifique à l’élément de travail sur la demande de firebase database? C’est souvent ce que permettent les bibliothèques d’API DB.

Votre moniteur de dépassement de délai doit restr informé de l’état de l’élément de travail. Éventuellement, une méthode virtuelle Cancel () sur votre élément de travail, pour assurer une flexibilité dans le nettoyage des éléments périmés.

Citant Herb Sutter:

Les listes chaînées sont des structures de données parfaitement adaptées à la concurrence, car elles prennent en charge des mises à jour hautement localisées. En particulier, comme illustré à la figure 1, pour insérer un nouveau nœud dans une liste à double liaison, il suffit de toucher deux nœuds existants. à savoir, ceux qui sont immédiatement adjacents à la position occupée par le nouveau nœud pour scinder le nouveau nœud dans la liste. Pour effacer un nœud, il suffit de toucher trois nœuds: celui qui est en train d’être effacé et ses deux nœuds immédiatement adjacents.

Cela dit, je suis d’accord avec les commentaires selon lesquels vous devriez probablement supprimer l’article de la file d’attente avant de le traiter. Mais je peux me tromper car je ne connais pas les détails de votre candidature.

Jetez un long regard sur la série ” Effective Concurrency ” de Herb Sutters (qui sera bientôt un livre).

Supprimez toujours les articles de la queue avant de consumr – vous ne mangez pas de pommes quand vous êtes encore sur l’arbre, n’est-ce pas?

En bref: lorsque vous supprimez des éléments de la liste / de la liste liée individuellement, utilisez une opération atomique de comparaison et d’échange ou, dans le langage Windows, InterlockedExchangePointer . Cela permettra toujours à un thread d’avancer. Il y a probablement des fonctions similaires dans Boost.

Déplacez également la connexion dans la classe effectuant la consommation.