3
votes

Meilleur moyen en C ++ de garder une grosse structure atomique?

J'ai une grande structure simple sans aucune méthode. Il comprend de nombreux champs et un autre conteneur (std :: vector). Je dois le rendre atomique, de manière à permettre à un thread producteur et à de nombreux threads consommateurs d'y accéder simultanément. les consommateurs ne modifient pas les données, ils lisent simplement les données. il n'y a qu'un seul producteur qui ajoute / modifie (ne supprime pas) les données, il doit donc conserver l'intégrité / la cohérence des données qui peuvent être vues par les consommateurs à tout moment.

Je pense qu'il y a deux solutions pour le résoudre, mais je ne sais pas laquelle est la meilleure.

Méthode 1:

auto& one_struct = all_structs1["some_id"];
one_struct.data_ok.store( false, memory_order_release );
// change the data....
one_struct.data_ok.store( true, memory_order_release );

Méthode 2:

// Single_t and Details_t are same as the ones in Method1.
struct BigStruct_2 {
   std::string id;
   size_t      count;
   Details_t   details;
};
std::map<std::string, std::atomic<BigStruct_2> > all_structs2;

Jusqu'à présent, je préfère Methods2 car je n'ai pas besoin de vérifier si data_ok est présent partout dans les codes des consommateurs . Infact consumer tient un pointeur vers un seul BigStructs_2, je veux que les données soient accessibles à tout moment (les données ne seront pas supprimées). Heureusement, il n'y a qu'un seul producteur dans mon système, donc le producteur peut simplement écraser les données en une seule étape: std::atomic<BigStruct_2>::store( tmp_pod ); , Je n'ai donc pas besoin de vérifier si les données sont prêtes / OK (cela devrait toujours être OK) lorsqu'un consommateur lit les données, et je n'ai pas non plus besoin d'utiliser std :: atomic :: compare_exchange_xxx dans dans ce cas, il n'est donc pas nécessaire de proposer un algorithme de comparaison entre deux objets BigStruct_2. Ai-je raison?

Si j'utilise Method1, lorsque je dois modifier les données, je dois coder comme suit:

struct Single_t {
   size_t idx;
   double val;
}
using Details_t = std::vector<Single_t>;

struct BigStruct_1 {
   std::string      id;
   size_t           count;
   Details_t        details;
   std::atomic_bool data_ok { false };
};
std::map<std::string, BigStruct_1> all_structs1;

et vérifiez si data_ok partout dans les codes des consommateurs ... c'est moche et dangereux (facile à oublier de vérifier).

Tous les recommandations / conseils seront appréciés!


3 commentaires

Dans un premier temps, ni BigPOD_0 ni BigPOD_1 sont des structures POD, car ils contiennent un membre des types non-POD std::string et Details_t


BigPOD_X côté pour le moment, on ne sait pas comment vous prévoyez d'éviter les courses de données sur all_pods0 / all_pods1 . Ce sont des structures de données partagées qui ne semblent en aucun cas protégées contre les accès simultanés. Il serait utile de préparer un exemple minimal reproductible .


Merci beaucoup! C'est mon erreur, je vais la corriger. J'écris POD ici car ce que je souligne en fait, c'est que cette structure n'a pas de méthodes, c'est-à-dire que c'est juste une structure simple à utiliser pour fournir des données.


4 Réponses :


1
votes

Je vous suggère de rendre vos BigStructs effectivement immuables et de les stocker comme ceci:

std::map<std::string, std::shared_ptr<BigStruct_2> > all_structs2;

Afin de faire une mise à jour, le thread d'écriture allouerait un nouveau BigStruct_2 (en utilisant std::make_shared ou autre), le définirait égal à l'objet qu'il veut modifier, puis modifierait le nouveau BigStruct_2 , puis insérerait le nouveau shared_ptr<BigStruct_2> dans la map .

Ensuite, chaque fois qu'un thread de lecture veut lire un BigStruct_2 il peut le faire en toute sécurité en tenant un std::shared_ptr<BigStruct_2> qu'il a récupéré à partir de std::map . Puisque votre thread d'écriture prend soin de ne jamais modifier un BigStruct_2 qu'il a déjà inséré dans la map , il n'y a aucune chance qu'un thread de lecture soit mal pris par le BigStruct_2 du contenu d'un BigStruct_2 alors qu'il est en cours de En le lisant.

Notez que rien de ce qui précède ne permet d'accéder à std::map lui-même thread-safe, vous devrez donc toujours synchroniser tous ceux avec un mutex. Ces actions devraient cependant être assez rapides et je doute donc que ce soit un problème.


1 commentaires

J'ai traité la condition de race de la carte moi-même, en ajoutant un mutex. Pour simplifier, je l'ai omis. Merci beaucoup!



2
votes

Une autre solution pour mettre à jour les objets partagés qui sont principalement lus est seqlock :

Les Seqlocks sont un mécanisme de synchronisation important et représentent une amélioration significative par rapport aux verrous lecteur-écrivain conventionnels dans certains contextes. Ils évitent la nécessité de mettre à jour une variable de synchronisation pendant une section critique du lecteur et améliorent ainsi les performances en évitant les erreurs de cohérence du cache sur l'objet de verrouillage lui-même.


2 commentaires

Merci pour votre recommandation! Le SeqLock est un nouvel élément pour moi, je l'ai choisi dans ma boîte à outils! Mais dans ce cas, je pense que ce n'est pas très approprié. Si un SeqLock est utilisé pour synchroniser les opérations avec std :: map, et qu'un thread de lecture peut toujours s'exécuter dans std :: map :: find () après avoir obtenu un numéro de séquence au lieu d'être bloqué lorsqu'un thread d'écriture écrit / rééquilibrer l'arborescence de std :: map, il peut alors planter à cause de données sales. Si SeqLock sert à protéger la cohérence de BigStruct, alors je dois mettre les codes de vérification partout chez le consommateur. Merci quand même!!!


@Leon Evidemment, seqlock est inutile pour les structures de données non triviales qui peuvent être transitoirement dans un état incohérent lors de la mise à jour et ne peuvent donc pas être lues simultanément pendant ce temps.



1
votes

std::atomic<BigStruct_2> utilisera effectivement lock: is_always_lock_free et is_lock_free sont très probablement false sur toute implémentation.

De plus, vous ne pouvez pas utiliser de type non copiable de toute façon dans std::atomic (c'est-à-dire pas de std::string dans votre structure).

Généralement, la capacité de std::atomic à travailler avec des types de n'importe quelle taille est pour la portabilité (dans le cas où HW ne prend pas en charge les atomiques de taille particulière, mais certains le font). Il n'est pas destiné à être utilisé dans un boîtier jamais sans verrouillage.

L'approche avec data_ok est également basée sur le verrouillage. Vous devrez réessayer lorsque data_ok est false.

Vous feriez mieux d'utiliser simplement std::mutex pour protéger les données, ou, si les lectures sont courantes, utilisez std::shared_mutex pour autoriser les lectures simultanées.


0 commentaires

0
votes

Si vous cherchez à minimiser absolument le temps de verrouillage, je suggérerais une troisième approche - vous avez une carte des éléments qui sont des structures contenant un pointeur atomique vers les valeurs actuelles, par opposition à une carte de pointeurs partagés.

L'avantage de faire cela est que vous n'avez pas besoin de verrouiller la carte pour mettre à jour les valeurs; il vous suffit de verrouiller la carte pour les ajouts et les suppressions.

Le verrouillage de la carte est en fait un verrou de domaine qui bloquerait tous les threads, alors que la plupart du temps, vous n'avez vraiment besoin que de bloquer tout thread lisant l'entrée de la carte qui vous intéresse.

Vous effectuez effectivement une "copie sur écriture" avec les données pour les modifier. Alors tu as

struct Single_t {
   size_t idx;
   double val;
}
using Details_t = std::vector<Single_t>;

struct BigStruct_Container {
   std::string      id;       //maybe put this in the digest if immutable?
   size_t           count;
   Details_t        details;
};

struct BigStruct_Digest {
   std::atomic<BigStruct_Container*> current_value;
};
std::map<std::string, BigStruct_Digest> all_digests;

Avec cela, pour effectuer une mise à jour, vous prenez une copie des données et générez un nouveau conteneur. Ensuite, vous mettez simplement à jour le pointeur de conteneur (résumé) qui se trouve dans la carte. Puisque vous ne modifiez pas la valeur de la carte, la carte reste constante, il n'est donc pas nécessaire de la verrouiller lors de la mise à jour.


Un mot d'avertissement avec l'utilisation de shared-ptrs; vous devez utiliser un mutex pour protéger le mécanisme de comptage de références car il n'est pas thread-safe. Si vous utilisez des verrous de domaine, cela devrait être correct, mais ce n'est pas une solution optimale.


Notez que, étant donné que la carte ne contient pas beaucoup de données, vous pouvez même effectuer une copie sur écriture avec la carte et avoir un atomic-ptr sur la carte elle-même .....


Cela va dans le sens de "verrouiller les membres individuels, pas la collection" dans le livre "Sept modèles de concurrence en sept semaines" - https://www.amazon.co.uk/Seven-Concurrency-Models-Weeks-Programmers/dp / 1937785653


4 commentaires

Les compteurs de référence utilisés par shared_ptr utilisent std::atomic fonctionnalité std::atomic et sont donc thread-safe sans nécessiter de mutex (bien que bien sûr les données vers lesquelles pointe shared_ptr ne soient pas sécurisées pour les threads simplement parce qu'un shared_ptr pointe vers elle).


Non, j'en ai moi-même été victime. Voir stackoverflow.com/a/47117985/1607937


Ils ont corrigé cela dans C ++ 20 avec atomic_shared_ptr je crois


Je pense que le commentaire de @ WhizTim sur votre lien contient la distinction critique; c'est-à-dire qu'il n'est pas sûr d'avoir deux threads accédant simultanément au même shared_ptr , mais il est sûr d'avoir deux shared_ptr , chacun accédant par un thread différent, tous deux référençant simultanément le même objet.