36 votes

Pourquoi std::shared_ptr::unique() est-il obsolète ?

Quel est le problème technique avec std::shared_ptr::unique() qui est la raison de son obsolescence en C++17 ?

Selon cppreference.com, std::shared_ptr::unique() est obsolète en C++17 car

cette fonction est obsolète depuis C++17 car use_count est seulement une approximation dans un environnement multi-thread.

Je comprends que c'est vrai pour use_count() > 1: Tant que je tiens une référence, quelqu'un d'autre pourrait la lâcher simultanément ou en créer une nouvelle.

~~Mais si use_count() retourne 1 (ce qui m'intéresse lorsque j'appelle unique()) alors il n'y a pas d'autre thread qui puisse changer cette valeur de manière hasardeuse, donc je m'attends à ce que cela devrait être sûr :

if (myPtr && myPtr.unique()) {
    //Modifier *myPtr
}~~ 

Résultats de ma propre recherche :

J'ai trouvé ce document : http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0521r0.html qui propose l'obsolescence en réponse au commentaire CA 14 du CD de C++17, mais je n'ai pas pu trouver ledit commentaire lui-même.

En alternative, ce document propose d'ajouter quelques notes incluant ce qui suit :

Note : Lorsque plusieurs threads peuvent influencer la valeur de retour de use_count(), le résultat doit être traité comme une approximation. En particulier, use_count() == 1 n'implique pas que les accès via un shared_ptr précédemment détruit ont été complétés d'une quelconque manière. — note de fin

Je comprends que cela pourrait être le cas pour la façon dont use_count() est actuellement spécifié (en raison du manque de synchronisation garantie), mais pourquoi la résolution n'a-t-elle pas simplement été de spécifier une telle synchronisation et donc rendre le modèle ci-dessus sûr ? S'il y avait une limitation fondamentale qui empêcherait une telle synchronisation (ou la rendrait excessivement coûteuse), alors comment est-il possible de mettre en œuvre correctement le destructeur ?

Mise à jour :

J'ai négligé le cas évident présenté par @alexeykuzmin0 et @rubenvb, car jusqu'à présent j'avais seulement utilisé unique() sur des instances de shared_ptr qui n'étaient pas accessibles à d'autres threads eux-mêmes. Donc il n'y avait pas de danger que cette instance particulière soit copiée de manière hasardeuse.

Je serais toujours intéressé par ce que CA 14 était exactement, car je crois que tous mes cas d'utilisation pour unique() fonctionneraient tant qu'il est garanti de se synchroniser avec ce qui arrive aux différentes instances de shared_ptr sur d'autres threads. Cela me semble donc toujours être un outil utile, mais je pourrais passer à côté de quelque chose de fondamental ici.

Pour illustrer ce que j'ai en tête, considérez ce qui suit :

class MemoryCache {
public:
    MemoryCache(size_t size)
        : _cache(size)
    {
        for (auto& ptr : _cache) {
            ptr = std::make_shared>();
        }
    }

    // le morceau de mémoire retourné peut être transmis à un autre thread(s),
    // mais la fonction n'est jamais accédée par deux threads simultanément
    std::shared_ptr> getChunk()
    {
        auto it = std::find_if(_cache.begin(), _cache.end(), [](auto& ptr) { return ptr.unique(); });
        if (it != _cache.end()) {
            //la mémoire n'est plus utilisée par l'utilisateur précédent, elle peut donc être donnée à quelqu'un d'autre
            return *it;
        } else {
            return{};
        }
    }
private:
    std::vector>> _cache;
};

Y a-t-il quelque chose qui ne va pas (si unique() se synchroniserait effectivement avec les destructeurs des autres copies) ?

1 votes

Pourquoi le 1 est-il un cas spécial ? Il pourrait y avoir une autre copie créée après votre appel à unique et avant que vous n'ayez fini ce que vous êtes en train de faire.

0 votes

@rubenvb: Si use_count == 1, alors il n'y a - par définition - aucun autre thread ayant une référence à partir de laquelle il pourrait faire une copie.

1 votes

@rubenvb: Mon erreur - unique est const, donc un autre thread pourrait en faire une copie sans qu'il s'agisse d'une course de données

11voto

alexeykuzmin0 Points 692

Considérez le code suivant:

// variable globale
std::shared_ptr s = std::make_shared();

// thread 1
if (s && s.unique()) {
    // modifier *s
}

// thread 2
auto s2 = s;

Ici, nous avons un problème classique de condition de course: s2 peut (ou non) être créé comme une copie de s dans le thread 2 tandis que le thread 1 est à l'intérieur du if.

Le unique() == true signifie que personne d'autre n'a un shared_ptr pointant vers la même mémoire, mais ne signifie pas que d'autres threads n'ont pas accès au shared_ptr initial directement ou par le biais de pointeurs ou de références.

0 votes

Merci. Je ne sais pas pourquoi j'ai négligé ce cas évident. Je ne vois toujours pas pourquoi la fonction devrait être obsolète, mais au moins la décision a plus de sens pour moi maintenant.

0 votes

J'ai mis à jour ma question un peu. Si vous avez des pensées à ce sujet, n'hésitez pas à les partager.

12 votes

Je ne comprends pas pourquoi cette réponse a tant de votes positifs. s.unique() == true n'a jamais été un moyen de dire que d'autres threads n'ont pas accès à s.

8voto

yohjp Points 1036

Je pense que P0521R0 résout potentiellement les courses de données en utilisant de manière incorrecte shared_ptr comme synchronisation entre threads. Il dit que use_count() renvoie une valeur de décompte de références peu fiable, et ainsi, la fonction membre unique() sera inutile en cas de multithreading.

int main() {
  int result = 0;
  auto sp1 = std::make_shared(0);  // décompte de références : 1

  // Démarrer un autre thread
  std::thread another_thread([&result, sp2 = sp1]{  // décompte de références : 1 -> 2
    result = 42;  // [W] stocker le résultat
    // [D] fin de la portée de sp2, et décompte de références : 2 -> 1
  });

  // Faire des opérations en multithreading:
  //   D'autres threads peuvent incrémenter/décrémenter le décompte de références.

  if (sp1.unique()) {      // [U] décompte de références == 1?
    assert(result == 42);  // [R] lecture du résultat
    // Cette action de lecture [R] provoque une course de données par rapport à l'action d'écriture [W].
  }

  another_thread.join();
  // Note : la fin du thread et la fonction membre join() ont une relation de précédence, donc [W] précède [R]
  // et il n'y a pas de course de données sur l'action de lecture suivante.
  assert(result == 42);
}

La fonction membre unique() n'a aucun effet de synchronisation et il n'y a pas de relation de précédence de [D] destructeur de shared_ptr à [U] appelant unique(). Donc nous ne pouvons pas attendre la relation [W] [D] [U] [R] et [W] [R]. ('' signifie une relation de précédence).


MODIFIÉ : J'ai trouvé deux problèmes LWG connexes ; LWG2434. shared_ptr::use_count() est efficiente, LWG2776. shared_ptr unique() et use_count(). C'est juste une spéculation, mais le Comité WG21 donne la priorité à l'implémentation existante de la bibliothèque standard C++, donc ils codifient son comportement dans C++1z.

LWG2434 citation (soulignement de ma part) :

shared_ptr et weak_ptr mentionnent que leur fonction use_count() peut être inefficace. Cela est une tentative de reconnaître les implémentations avec reflinkage (qui peuvent être utilisées par des pointeurs intelligents Loki, par exemple). Cependant, il n'y a pas d'implémentations de shared_ptr qui utilisent le reflinkage, surtout après que C++11 a reconnu l'existence du multithreading. Tout le monde utilise des décomptes de références atomiques, donc use_count() est juste une charge atomique.

LWG2776 citation (soulignement de ma part) :

La suppression de la restriction "uniquement pour le débogage" pour use_count() et unique() dans shared_ptr par LWG 2434 a introduit un bug. Pour que unique() produise une valeur utile et fiable, il a besoin d'une clause de synchronisation pour garantir que les accès précédents via une autre référence sont visibles pour l'appelant réussi de unique(). Beaucoup d'implémentations actuelles utilisent une charge relaxée, et ne fournissent pas cette garantie, car ce n'est pas spécifié dans la norme. Pour une utilisation de débogage/conseil, c'était correct. Sans cela, la spécification est imprécise et probablement trompeuse.

[...]

Je préférerais spécifier use_count() comme fournissant uniquement un indice peu fiable du décompte réel (une autre façon de dire uniquement pour le débogage). Ou la déprécier, comme JF l'a suggéré. Nous ne pouvons pas rendre use_count() fiable sans ajouter de manière substantielle plus de barrières. Nous ne voulons vraiment pas de quelqu'un attendant que use_count() == 2 pour déterminer qu'un autre thread en est arrivé là. Et malheureusement, je ne pense pas que nous disions actuellement quelque chose pour rendre clair que c'est une erreur.

Cela impliquerait que use_count() utilise normalement memory_order_relaxed, et que unique n'est ni spécifié ni implémenté en termes de use_count().

1 votes

C'est en fait un point intéressant. Quiconque utilise un pointeur partagé de la manière dont le fait votre exemple de démonstration devrait probablement être giflé, mais il pourrait y avoir des motifs plus complexes où cette supposition est faite de manière implicite quelque part. J'aurais quand même préféré qu'ils corrigent unique() en spécifiant un effet de synchronisation au lieu de le rendre obsolète, mais il pourrait y avoir des implications en termes de performances que je ne vois pas.

0 votes

Merci d'avoir déterré ces commentaires

2 votes

C'est totalement faux: "donc, la fonction membre unique() sera inutile en cas de multithreading". Non, unique() n'est pas utile pour la synchronisation. Cela ne signifie pas du tout que c'est "inutile". J'espère vraiment que vous ne vouliez pas dire cela, car alors tout ce qui ne peut pas être utilisé pour la synchronisation serait "inutile en cas de multithreading".

4voto

Mikel F Points 2907

Pour votre plaisir de visionner: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0488r0.pdf

Ce document contient tous les commentaires des Organismes de Normalisation (ON) pour la réunion d'Issaquah. CA 14 dit:

La suppression de la restriction "uniquement pour le débogage" pour use_count() et unique() dans shared_ptr a introduit un bogue: afin que unique() produise une valeur utile et fiable, il a besoin d'une clause de synchronisation pour garantir que les accès précédents via une autre référence sont visibles pour l'appelant réussi de unique(). De nombreuses implémentations actuelles utilisent une charge détendue, et ne fournissent pas cette garantie, car elle n'est pas précisée dans la norme. Pour une utilisation de débogage/conseils, c'était OK. Sans cela, la spécification est peu claire et trompeuse.

1 votes

Merci beaucoup - Je me demande pourquoi le comité n'a pas retenu la première solution proposée dans le commentaire : "Une solution pourrait consister à faire en sorte que unique() utilise memory_order_acquire, et à spécifier que les opérations de décrémentation du comptage des références se synchronisent avec unique()."

0 votes

@MikeMB Je crains que mes recherches n'aient rien révélé qui puisse éclairer leur motivation. Les informations peuvent exister, mais ce n'est pas un fruit facile à cueillir.

1 votes

La synchronisation nécessaire pour cela pourrait rendre assez pessimistes certaines opérations assez courantes (comme la copie), pour permettre un abus de unique/use_count qui n'était de toute façon jamais prévu.

1voto

Philippe Points 127

L'existence de std::enable_shared_from_this est ce qui pose problème pour rendre tout usage intéressant de unique(). En effet, std::enable_shared_from_this permet de créer un nouveau shared_ptr à partir d'un pointeur brut, de n'importe quel thread. Cela signifie que unique() ne peut jamais garantir quoi que ce soit.

Mais considérez une autre bibliothèque... Même s'il ne s'agit pas de shared_ptr, dans Qt, il y a une méthode interne appelée isDetached() avec (presque) la même implémentation que unique(). Elle est utilisée pour une optimisation assez utile : lorsque true, l'objet pointé peut être modifié sans effectuer d'opération "copy-on-write". En effet, une fois unique, une ressource gérée ne peut pas devenir partagée par une action provenant d'un autre thread. Le même schéma serait possible avec shared_ptr si enable_shared_from_this n'existait pas.

C'est pourquoi selon moi, unique() a été supprimé de C++20 : trompeur.

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