J'ai un wrapper pour un morceau de code hérité.
class A{
L* impl_; // the legacy object has to be in the heap, could be also unique_ptr
A(A const&) = delete;
L* duplicate(){L* ret; legacy_duplicate(impl_, &L); return ret;}
... // proper resource management here
};
Dans ce code hérité, la fonction qui «duplique» un objet n'est pas thread-safe (lors de l'appel au même premier argument), elle n'est donc pas marquée const
dans le wrapper. Je suppose que les règles modernes suivantes: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Cela duplicate
ressemble à un bon moyen d'implémenter un constructeur de copie, à l'exception du détail qui ne l'est pas const
. Je ne peux donc pas le faire directement:
class A{
L* impl_; // the legacy object has to be in the heap
A(A const& other) : L{other.duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Alors, comment sortir de cette situation paradoxale?
(Disons aussi que ce legacy_duplicate
n'est pas thread-safe mais je sais que l'objet reste dans son état d'origine à sa sortie. Étant une fonction C, le comportement est seulement documenté mais n'a pas de concept de constance.)
Je peux penser à de nombreux scénarios possibles:
(1) Une possibilité est qu'il n'y a aucun moyen d'implémenter un constructeur de copie avec la sémantique habituelle. (Oui, je peux déplacer l'objet et ce n'est pas ce dont j'ai besoin.)
(2) D'un autre côté, la copie d'un objet est intrinsèquement non thread-safe dans le sens où la copie d'un type simple peut trouver la source dans un état semi-modifié, donc je peux simplement avancer et le faire peut-être,
class A{
L* impl_;
A(A const& other) : L{const_cast<A&>(other).duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(3) ou même simplement déclarer duplicate
const et mentir sur la sécurité des threads dans tous les contextes. (Après tout, la fonction héritée ne se soucie pas, const
donc le compilateur ne se plaindra même pas.)
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Enfin, je peux suivre la logique et créer un constructeur de copie qui prend un argument non const .
class A{
L* impl_;
A(A const&) = delete;
A(A& other) : L{other.duplicate()}{}
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Il s'avère que cela fonctionne dans de nombreux contextes, car ces objets ne le sont généralement pas const
.
La question est, est-ce un itinéraire valable ou commun?
Je ne peux pas les nommer, mais je m'attends intuitivement à beaucoup de problèmes sur la route d'avoir un constructeur de copie non-const. Il ne sera probablement pas considéré comme un type de valeur en raison de cette subtilité.
(5) Enfin, bien que cela semble être une exagération et pourrait avoir un coût d'exécution élevé, je pourrais ajouter un mutex:
class A{
L* impl_;
A(A const& other) : L{other.duplicate_locked()}{}
L* duplicate(){
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
L* duplicate_locked() const{
std::lock_guard<std::mutex> lk(mut);
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
mutable std::mutex mut;
};
Mais être forcé de faire cela ressemble à de la pessimisation et agrandit la classe. Je ne suis pas sûr. Je penche actuellement vers (4) ou (5) ou une combinaison des deux.
—— MODIFIER
Une autre option:
(6) Oubliez tout le non-sens de la fonction membre en double et appelez simplement à legacy_duplicate
partir du constructeur et déclarez que le constructeur de copie n'est pas thread-safe. (Et si nécessaire, faites une autre version thread-safe du type, A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
EDIT 2
Cela pourrait être un bon modèle pour ce que fait la fonction héritée. Notez qu'en touchant l'entrée, l'appel n'est pas thread-safe par rapport à la valeur représentée par le premier argument.
void legacy_duplicate(L* in, L** out){
*out = new L{};
char tmp = in[0];
in[0] = tmp;
std::memcpy(*out, in, sizeof *in); return;
}
L
lequel est modifié en créant une nouvelleL
instance? Sinon, pourquoi pensez-vous que cette opération n'est pas thread-safe?legacy_duplicate
ne peut pas être appelée avec le même premier argument à partir de deux threads différents.const
vraiment ce que cela signifie. :-) Je ne penserais pas à deux fois avant de prendre unconst&
dans mon ctor de copie tant que je ne modifie pasother
. Je pense toujours à la sécurité des threads comme quelque chose que l'on ajoute en plus de tout ce qui doit être accessible à partir de plusieurs threads, via l'encapsulation, et j'attends vraiment les réponses avec impatience.Réponses:
J'inclurais simplement vos options (4) et (5), mais vous vous engagez explicitement à adopter un comportement non thread-safe lorsque vous pensez que cela est nécessaire pour les performances.
Voici un exemple complet.
Production:
Cela suit le guide de style Google dans lequel
const
communique la sécurité des threads, mais le code appelant votre API peut se désinscrire en utilisantconst_cast
la source
legacy_duplicate
pourrait êtrevoid legacy_duplicate(L* in, L** out) { *out = new L{}; char tmp = in[0]; /*some weird call here*/; in[0] = tmp; std::memcpy(*out, in, sizeof *in); return; }
(c'est-à-dire non-constin
)A a2(a1)
pourrait essayer d'être sûr pour les threads (ou être supprimé) etA a2(const_cast<A&>(a1))
n'essaierait pas du tout d'être sûr pour les threads.A
à la fois dans des contextes thread-safe et thread-unsafe, vous devez tirer leconst_cast
vers le code appelant afin qu'il soit clair où la thread-safety est connue pour être violée. Il est correct de pousser la sécurité supplémentaire derrière l'API (mutex) mais pas correct de masquer l'insécurité (const_cast).TLDR: Soit corriger l'implémentation de votre fonction de duplication, soit introduire un mutex (ou un dispositif de verrouillage plus approprié, peut-être un spinlock, ou assurez-vous que votre mutex est configuré pour tourner avant de faire quoi que ce soit de plus lourd) pour l'instant , puis corrigez l'implémentation de la duplication et retirez le verrouillage lorsque le verrouillage devient réellement un problème.
Je pense qu'un point clé à noter est que vous ajoutez une fonctionnalité qui n'existait pas auparavant: la possibilité de dupliquer un objet à partir de plusieurs threads en même temps.
Évidemment, dans les conditions que vous avez décrites, cela aurait été un bug - une condition de concurrence, si vous l'aviez fait auparavant, sans utiliser une sorte de synchronisation externe.
Par conséquent, toute utilisation de cette nouvelle fonctionnalité sera quelque chose que vous ajouterez à votre code, et non pas héritée en tant que fonctionnalité existante. Vous devriez être celui qui sait si l'ajout du verrouillage supplémentaire sera réellement coûteux - selon la fréquence à laquelle vous allez utiliser cette nouvelle fonctionnalité.
En outre, en fonction de la complexité perçue de l'objet - par le traitement spécial que vous lui donnez, je vais supposer que la procédure de duplication n'est pas triviale, donc, déjà assez coûteuse en termes de performances.
Sur la base de ce qui précède, vous avez deux voies que vous pouvez suivre:
A) Vous savez que la copie de cet objet à partir de plusieurs threads ne se produira pas assez souvent pour que la surcharge du verrouillage supplémentaire soit coûteuse - peut-être trivialement bon marché, du moins étant donné que la procédure de duplication existante est assez coûteuse en soi, si vous utilisez un spinlock / pré-rotation mutex, et il n'y a aucune contestation à ce sujet.
B) Vous pensez que la copie à partir de plusieurs threads se produira assez souvent pour que le verrouillage supplémentaire soit un problème. Ensuite, vous n'avez vraiment qu'une seule option: corriger votre code de duplication. Si vous ne le corrigez pas, vous devrez de toute façon verrouiller, que ce soit sur cette couche d'abstraction ou ailleurs, mais vous en aurez besoin si vous ne voulez pas de bugs - et comme nous l'avons établi, dans ce chemin, vous supposez ce verrouillage sera trop coûteux, par conséquent, la seule option consiste à corriger le code de duplication.
Je soupçonne que vous êtes vraiment dans la situation A, et l'ajout d'un mutex spinlock / spinning qui n'a pratiquement aucune pénalité de performance lorsqu'il n'est pas contesté, fonctionnera très bien (n'oubliez pas de le comparer, cependant).
Il y a, en théorie, une autre situation:
C) Contrairement à la complexité apparente de la fonction de duplication, elle est en fait triviale, mais ne peut pas être corrigée pour une raison quelconque; il est si trivial que même un verrou tournant non contesté introduit une dégradation des performances inacceptable à la duplication; la duplication sur les threads parallèles est rarement utilisée; la duplication sur un seul thread est utilisée tout le temps, ce qui rend la dégradation des performances absolument inacceptable.
Dans ce cas, je suggère ce qui suit: déclarer les constructeurs / opérateurs de copie par défaut supprimés, pour empêcher quiconque de les utiliser accidentellement. Créez deux méthodes de duplication explicitement appelables, une méthode thread-safe et une thread thread unsafe; incitez vos utilisateurs à les appeler explicitement, selon le contexte. Encore une fois, il n'y a aucun autre moyen d'obtenir des performances acceptables pour un seul thread et un multithread sûr, si vous êtes vraiment dans cette situation et que vous ne pouvez tout simplement pas corriger l'implémentation de duplication existante. Mais je pense qu'il est très peu probable que vous le soyez vraiment.
Ajoutez simplement ce mutex / spinlock et référence.
la source
std::mutex
? La fonction en double n'est pas un secret, je ne l'ai pas mentionnée pour garder le problème à un niveau élevé et ne pas recevoir de réponses sur MPI. Mais depuis que vous êtes allé aussi loin, je peux vous donner plus de détails. La fonction héritée estMPI_Comm_dup
et la sécurité non-thread efficace est décrite ici (je l'ai confirmé) github.com/pmodels/mpich/issues/3234 . C'est pourquoi je ne peux pas corriger les doublons. (De plus, si j'ajoute un mutex, je serai tenté de rendre tous les appels MPI sécurisés pour les threads.)