36 votes

Pourquoi std::shared_ptr::unique() est-elle 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 n'est qu'une approximation dans un environnement multi-thread.

Je comprends que cela est vrai pour use_count() > 1: pendant que je détiens une référence, quelqu'un d'autre pourrait simultanément la lâcher 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 façon hasardeuse, donc je m'attendrais à ce que cela soit 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.

Comme alternative, ce document proposait d'ajouter quelques notes, y compris les suivantes :

Note : Lorsque plusieurs threads peuvent affecter la valeur de retour de use_count(), le résultat devrait ê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 de quelque manière que ce soit. — 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 de rendre ainsi le modèle ci-dessus sûr ? S'il y avait une limitation fondamentale qui n'aurait pas permis une telle synchronisation (ou la rendait prohibitivement coûteuse), comment serait-il possible d'implémenter correctement le destructeur ?

Mise à jour :

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

Je serais toujours intéressé de savoir de quoi CA 14 parlait exactement, car je crois que tous mes cas d'utilisation pour unique() fonctionneraient tant qu'il est garanti de se synchroniser avec ce qui se passe avec différentes instances de shared_ptr sur d'autres threads. Cela me semble donc toujours être un outil utile, mais je pourrais négliger 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é pourrait être transmis à un/des thread(s) différents,
    // mais la fonction n'est jamais accédée par deux threads en même temps
    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, donc elle peut ê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() synchronise en réalité avec les destructeurs d'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 ayez terminé 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 qui possède une référence à partir de laquelle il pourrait faire une copie.

1 votes

@rubenvb : Mon erreur - unique est constant, donc un autre thread pourrait faire une copie sans que ce soit 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 une condition de course classique: s2 peut (ou non) être créé comme une copie de s dans le thread 2 pendant que le thread 1 est à l'intérieur du if.

Le unique() == true signifie que personne 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 via des pointeurs ou des 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 me semble maintenant plus logique.

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 autant de votes favorables. s.unique() == true n'a jamais prétendu signifier que aucun autre fil ne peut accéder à s.

8voto

yohjp Points 1036

Je pense que P0521R0 résout potentiellement les courses aux données en utilisant de manière incorrecte shared_ptr comme synchronisation inter-thread. Il dit que use_count() renvoie une valeur de compteur de références non fiable, et donc, la fonction membre unique() sera inutile en cas de multithreading.

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

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

  // Faire des choses en multithreading :
  //   D'autres threads peuvent augmenter/diminuer le compteur de références en même temps.

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

  another_thread.join();
  // Note secondaire : la terminaison de thread et la fonction membre join()
  // ont une relation de happens-before, donc [W] arrive avant [R]
  // et il n'y a pas de course aux 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 happens-before de [D] destructeur de shared_ptr à [U] appelant unique(). Nous ne pouvons donc pas attendre une relation [W] [D] [U] [R] et [W] [R]. ('' indique une relation happens-before).


MODIFIÉ : J'ai trouvé deux problèmes LWG liés ; LWG2434. shared_ptr::use_count() est efficace, LWG2776. shared_ptr unique() et use_count(). Ce n'est qu'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 ont des Notes indiquant que leur use_count() pourrait être inefficace. C'est une tentative de reconnaître les implémentations reflinkées (qui peuvent être utilisées par les pointeurs intelligents Loki, par exemple). Cependant, il n'y a pas d'implémentations de shared_ptr qui utilisent le reflinking, surtout après que C++11 a reconnu l'existence du multithreading. Tout le monde utilise des compteurs de références atomiques, donc use_count() est juste une charge atomique.

LWG2776 citation (soulignement de ma part) :

La suppression de la restriction "debug only" pour use_count() et unique() dans shared_ptr par LWG 2434 a introduit un bogue. Pour que unique() produise une valeur utile et fiable, il faut une clause de synchronisation pour garantir que les accès antérieurs via une autre référence sont visibles pour l'appelant réussi de unique(). De nombreuses implémentations actuelles utilisent un chargement relaxé et ne fournissent pas cette garantie, car elle n'est pas spécifiée dans la norme. Pour une utilisation de débogage/conseil, c'était correct. Sans cela, la spécification est ambiguë et probablement trompeuse.

[...]

Je préférerais spécifier que use_count() fournit seulement un indice non fiable du nombre réel (une autre manière de dire seulement pour le débogage). Ou le rendre obsolète, comme JF l'a suggéré. Nous ne pouvons pas rendre use_count() fiable sans ajouter beaucoup plus de barrières. Nous ne voulons vraiment pas que quelqu'un attende 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 indiquer clairement 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 se faire fouetter, mais il pourrait y avoir des schémas plus complexes, où cette hypothèse est faite implicitement 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 déprécier, mais il pourrait y avoir des implications de performance que je ne vois pas.

0 votes

Merci d'avoir déterré ces commentaires

2 votes

C'est totalement faux : "Ainsi, 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 sincèrement 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 visualisation : http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0488r0.pdf

Ce document contient tous les commentaires des NB (National Body) pour la réunion d'Issaquah. CA 14 lit :

La suppression de la restriction "debug uniquement" 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 relaxée et ne fournissent pas cette garantie, car cela n'est pas précisé dans la Norme. Pour une utilisation de débogage/suggestion, c'était correct. 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 faire en sorte que unique() utilise memory_order_acquire et spécifie que les opérations de décrément de compteur de référence se synchronisent avec unique()."

0 votes

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

1 votes

La synchronisation nécessaire pour cela rendrait plus pessimistes certaines opérations assez courantes (comme la copie), pour permettre une utilisation abusive d'unique/use_count qui n'était de toute façon jamais prévue.

1voto

Philippe Points 127

L'existence de std::enable_shared_from_this est ce qui complique l'utilisation de unique(). En effet, std::enable_shared_from_this permet de créer un nouveau shared_ptr à partir d'un pointeur brut, depuis 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 si ce n'est pas à propos de shared_ptr, dans Qt, il y a une méthode interne appelée isDetached() avec une implémentation (presque) identique à celle de unique(). Elle est utilisée à des fins d'optimisation assez utiles : quand elle est true, l'objet pointé peut être muté sans effectuer une 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 à mon avis, unique() a été supprimé de C++20 : trompeur.

1 votes

Cela s'applique également à n'importe quel weak_ptr (qui est ce que enable_shared_from_this stocke finalement), car unique ne compte pas les références faibles comme "utilisations".

0 votes

Je ne comprends pas la connexion. Juste parce qu'un objet est géré par un shared_ptr ne signifie pas qu'il hérite de enable_shared_from_this.

0 votes

@MikeMB : c'est juste, mais je cite 'enable_shared_from_this' pour justifier que 'unique()' ne peut pas être une garantie pour quoi que ce soit, car les API de shared_ptr pourraient modifier le compteur de référence dans des threads différents, que ce soit pour l'incrémenter ou le décrémenter.

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