6
votes

Est-ce une version de verrouillage à double cochure C ++ 11 correcte avec Shared_PTR?

Ce article par Jeff supphing stipule que Le motif de verrouillage à double vérification (DCLP) est fixé en C ++ 11. L'exemple classique utilisé pour ce modèle est le modèle singleton, mais j'arrive à avoir un cas d'utilisation différent et je manque toujours d'expérience dans la manipulation "atomique <> armes" - peut-être que quelqu'un ici peut m'aider.

est la suivante Pièce de code Une implémentation correcte DCLP comme décrit par Jeff sous " à l'aide de C ++ 11 atomiques cohérents séquentiellement "? xxx


8 commentaires

Si vous gardez des données avec un mutex, vous n'avez pas besoin du atomic_store .


user_count est const , donc par les règles de la bibliothèque générale, l'appelant simultanément ne provoque pas de course de données.


Notez que votre code est au mieux "le meilleur effort". La condition data.use_count ()> 1 est pas corrigé par le mutex! Les copies existantes peuvent être détruites simultanément.


@Kerreksb j'ai entendu parler du const Remarque dans les herbes Talk . Seriez-vous vraiment aller jusqu'à supposer que cette règle est généralement mise en œuvre? Merci pour l'indice sur le atomic_store - j'ai négligé cela.


Eh bien, [res.on.data.races] / 2 exige que les fonctions de Cons-Membres ne modifient pas les objets. Cependant, cela est un peu flou, car la copie de données change clairement le nombre d'utilisations. Mais le constructeur de copie prend une valeur de const, alors je suppose que cela signifie que cela ne constitue pas une modification pertinente pour les courses de données. En pratique, je m'attends à ce que cela soit gratuit, mais ce n'est pas tout à fait clair de la norme autant que je puisse dire.


Ah, tout va bien. Tout d'abord, le paragraphe cité indique essentiellement que le constructeur de copie ne "modifie pas" l'argument (car c'est Const). Il y a une clarification dans [util.smartptr.shared] / 4: "Aux fins de déterminer la présence d'une course de données, les fonctions membres doivent accéder et modifier uniquement le partagé_ptr et faibles_ptr Objets eux-mêmes et non des objets qu'ils se réfèrent. Modifications de use_count () ne reflètent pas les modifications pouvant introduire des courses de données. "


@Kerreksb: Appeler une fonction const concurrente avec une fonction non- const (telle que opérateur = comme c'est le cas ici) peut < / i> causer une course de données


@Chrisdodd: Bien sûr, mais ce n'est pas ce qui se passe ici. L'accès modifiant est sérialisé via le mutex. L'aspect intéressant est que même s'il ne s'agissait jamais des vues const de données , la valeur de data.use_count () peut toujours changer.


3 Réponses :


0
votes

Selon votre source, je pense que vous devez toujours ajouter des clôtures de fil avant le premier test et après le deuxième test.

std::shared_ptr<B> data;
std::mutex mutex;

void detach()
{
  std::atomic_thread_fence(std::memory_order_acquire);
  if (data.use_count() > 1)
  {
    auto lock = std::lock_guard<std::mutex>{mutex};
    if (data.use_count() > 1)
    {
      std::atomic_thread_fence(std::memory_order_release);
      data = std::make_shared<B>(*data);
    }
  }
}


3 commentaires

Je pense std :: partagé_ptr :: user_count () est équivalent à un appel interne à std :: atomique :: charge () . Si tel est vrai, alors cela section devrait s'appliquer et aucune clôture n'est requise. Ou je manque quelque chose?


@HAUKEHEIBEL Je ne sais pas, je n'ai trouvé aucune référence qui soit soutenue ni contredire que. As-tu ?


@Nielk Que se passe-t-il si après le premier si , un autre thread favorise un faibly_ptr à data à un partagé_ptr ?



2
votes

Non, ce n'est pas une implémentation correcte de DCLP.

Le truc est que votre Vérification externe externe strud> data.use_count ()> 1 code> Accède au Objet strong> (de type B avec le nombre de références code>), qui peut être supprimé (non référencé) dans une partie protégée mutex. Toute sorte de clôtures de mémoire ne peut y aider là-bas. P>

pourquoi strong> data.use_counte () accède à l'objet em>: p>

suppose ces opérations ont été exécutés: p> xxx pré>

alors vous avez la mise en page suivante ( affaiblé_ptr code> ne figure pas ici): p>

/* Thread 1 */                   /* Thread 2 */       /* Thread 3 */
Enter detach()                   Enter detach()
Found `data.use_count()` > 1     
Enter critical section                                   
Found `data.use_count()` > 1
                                 Dereference `data`,
                                 found old object.
Unreference old `data`,
`use_count` becomes 1 
                                                      Delete other shared_ptr,
                                                      old object is deleted
Assign new object to `data`
                                 Access old object
                                 (for check `use_count`)
                                 !! But object is freed !!


14 commentaires

Tous les joueurs, c'est-à-dire Data , MUTEX et DÉTACH () sont censés être membres d'une classe. Ainsi, il n'y a aucun moyen que data soit supprimé si détacher () est appelé. Je vais aborder cette question en modifiant la question.


Il n'y a aucun moyen, ces données sont supprimées si détachez () est appelée . Je veux dire, objet référencé par données est non fermé, car vous attribuez une nouvelle valeur à Data . Donc, data.use_count () peut accéder à l'objet, tandis que Shared_PTR ne le pointe pas. C'est une mauvaise utilisation de Shared_PTR et peut conduire à un accès à la mémoire libérée, si d'autres Shared_Ptr's, indiqués sur cet objet sont supprimés au même moment.


Si je ne me trompe pas, votre scénario n'est possible que si le fil 1 ou le fil 2 travaux sur une référence à FOO - une référence appartenant à un autre fil. Votre exemple suppose en outre que le fil de possession de FOO le tue pendant que le fil tenant la référence y travaille toujours. Si tel est le cas, alors quelque chose est de toute façon substantiellement brisé.


Si votre référence Data appartient et accédée uniquement par un seul thread, pourquoi vous utilisez le verrouillage? Voir le point 2 à la fin de ma réponse.


Je n'ai jamais dit que l'objet ne doit pas être utilisé par plusieurs threads, mais il ne devrait certainement pas être supprimé alors que l'un des threads y accède - il n'y a aucune protection contre ce cas d'utilisation. Pour être plus clair, le cas d'utilisation que je vis avec foo est une implémentation de copie-écriture qui tire parti du fait que std :: partagé_ptr est déjà référence compté dans un fil de fil de sécurité. Le grand piège est le logement de fil de fil de sécurité sur des appels de fonctions membres non constitutifs sur foo . Un problème sans le mutex est par exemple créer plusieurs copies où on aurait été suffisant.


Essayons de clarifier les problèmes restants. Pour exclure le scénario thread 3 Supposons que foo n'est jamais détruit alors qu'il est utilisé par deux threads. Éliminons également votre point 2. - Comme vous l'avez déjà observé, toute la question serait discutable dans ce scénario. En ce qui concerne votre explication, il existe un malentendu du code. Vérification extérieure Ne jamais accéder à B , user_count () est une fonction de membre de partagé_ptr .


Je ne reçois pas le point 1 puisque l'un ou l'autre des threads multiples, il suffit de lire B qui est bien ou au moins un fil tente d'écrire sur B (c.-à-d. Utiliser_count ()> 1 dans un multi Scénario organisé). Dans ce dernier cas, se détach () garantit que le thread écrit à un B car après avoir appelé détach () , use_count () == 1 .


J'ai mis à jour la réponse avec l'explication pourquoi .uuse_count () accède à l'objet (de type B avec le nombre de références ). Si vous souhaitez empêcher cet objet de la suppression pendant détach () appel, qui change éventuellement des données , vous devez avoir un autre Shared_ptr < / code> pointé dessus. Mais avoir un autre partagé_ptr vous forcer data.use_count ()> 1 est toujours vrai. Les points à la fin de ma réponse décrivent les accès à B uniquement via données . Accès via autre Shared_PTR S ne sont pas pris en compte. J'espère que ces explications aideront.


Merci d'avoir essayé de clarifier mes questions. Ma compréhension actuelle est que vous décrivez le problème général des références pendantes. Ce problème n'est pas satisfait de "toute sorte de clôtures de mémoire" comme indiqué dans la réponse et donc inhérente à toute utilisation d'un mutex au sein d'une fonction membre - il n'est pas spécifique de DCLP. La réponse indique un problème de programmation valide et général. Ma question est vraiment si'use_Count () 'peut provoquer une course avec la mission de déplacement ou la construction de la construction d'A Hered_ptr'.


Oui, votre compréhension est correcte: le problème général de votre code est des références dangereuses. data.use_count () n'est pas couru avec data1 = données; Copier la construction. Accès simultané à data.use_count () et n'importe quelle méthode, modifiant data (E.G., data = DATA1; DATA1; Il peut être traité comme une course entre l'accès à la mémoire et libérer cette mémoire.


Je pense que nous sommes sur la bonne voie. Pouvons-nous convenir que nous ignorons le cas d'utilisation où le user_count () tombe sur 0? Si nous ne le faisons pas que ce code ne peut pas casser {lock_guard verrou {m_mutex}; autre_code (); } . Le destructeur Lock_Guard appelle std :: mutex :: déverrouiller () sur m_mutex qui est parti si la classe possède m_mutex est supprimé pendant que certains appels de threads autre_code () . Mon point est que la langue n'offre aucune aide pour ce scénario et que nous devrions l'ignorer.


Vous mal comprendre user_count Protection: il protège l'objet B plus le compteur lui-même d'être libéré. Objet FOO n'est pas supprimé automatiquement lorsque user_count gouttes à 0, donc mutex est vivant.


Je comprends use_count () assez bien. Je ne connais pas d'autre solution, mais de montrer un exemple de code qui démontre que votre scénario peut toujours planter: Ideone.com/tbyep (veuillez noter que vous devez tester localement car Ideone ne prend pas en charge la multi-threading). Si le fil 3 tue foo , c'est le jeu. Le partagé_ptr est privé et ne peut être détruit que via le destructeur FOO .


Je pense enfin à attraper enfin ce que vous voulez: chaque objet foo est accessible à partir d'un seul thread, et si foo foo1, foo2 = foo1; , alors un seul des simultané foo1.detach () et foo2.detach () devrait créer un nouvel objet B; autre devrait réutiliser l'objet actuel. Si tel est le cas, votre DCLP d'origine devient complètement correct après avoir fabriqué mutex statique.



1
votes

Ceci constitue une course de données si deux threads invoquent détacher sur la même instance de FOO simultanément, car std :: partagé_ptr :: user_count ( ) (une opération en lecture seule) fonctionnerait simultanément avec le std :: Shared_ptr Opérateur d'affectation de déplacement (une opération de modification), qui est une course de données et donc une cause de comportement non défini. Si FOO n'est jamais accessible simultanément, d'autre part, il n'y a pas de course de données, mais le std :: mutex serait inutile dans votre exemple. La question est la suivante: comment data est-il partagé en premier lieu? Sans ce bit crucial d'informations, il est difficile de dire si le code est en sécurité, même si un FOO n'est jamais utilisé simultanément.


1 commentaires

J'ai initialement utilisé ce code std :: atomic_store (& data, std :: make_shared (* autre.data)); au lieu de l'affectation de déplacement. Si plusieurs threads créent des copies de foo Vous avez probablement raison et que nous devons mettre en œuvre la construction, la copie et l'affectation de foo tout en termes de std :: atomic_load / stocker / stocker . Peut-être que quelqu'un d'autre peut confirmer cela avant de modifier la publication? @Kerreksb a suggéré que le «std: atomic_store» soit inutile mais je ne suis plus sûr du moins pas lorsque vous envisagez toute la classe.