36 votes

Pourquoi std::shared_ptr::unique() est-il déprécié?

Quel est le problème technique avec std::shared_ptr::unique() qui est la raison de sa dépréciation en C++17?

Selon cppreference.com, std::shared_ptr::unique() est déprécié en C++17 car

cette fonction est dépréciée à partir de C++17 car use_count est seulement 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 peut simultanément la lâcher ou en créer une nouvelle

~~Mais si use_count() renvoie 1 (ce qui m'intéresse lorsque j'appelle unique()) alors il n'y a pas d'autre thread qui pourrait changer cette valeur de manière aléatoire, 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 la dépréciation en réponse au commentaire CA 14 du CD 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 affecter la valeur renvoyée par use_count(), le résultat doit être traité comme une approximation. En particulier, use_count() == 1 n'implique pas que les accès à travers un shared_ptr précédemment détruit ont été complétés de quelque manière que ce soit. — fin note

Je comprends que cela pourrait être le cas pour la manière dont use_count() est actuellement spécifié (en raison du manque de synchronisation garantie), mais pourquoi la résolution n'a-t-elle pas été simplement de spécifier une telle synchronisation et ainsi rendre le motif ci-dessus sûr? S'il y avait une limitation fondamentale qui n'autoriserait pas une telle synchronisation (ou la rendrait prohibitivement coûteuse), comment est-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. Ainsi, il n'y avait aucun danger que cette instance particulière soit copiée de manière aléatoire.

Je serais toujours intéressé d'entendre ce que CA 14 était précisément, car je crois que tous mes cas d'utilisation de 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. Il semble donc toujours être un outil utile pour moi, mais je pourrais négliger quelque chose de fondamental ici.

Pour illustrer ce que j'ai à l'esprit, 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 passé à un/des thread(s) différent(s),
    // mais la fonction n'est jamais accédée depuis 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 de mal avec cela (si unique() synchroniserait en réalité avec les destructeurs d'autres copies)?

1 votes

Pourquoi 1 est 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 faites.

0 votes

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

1 votes

@rubenvb: Mon erreur - unique est const, 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 d'autre n'a de shared_ptr pointant vers la même mémoire, mais cela ne signifie pas que d'autres threads n'ont pas accès au shared_ptr initial directement ou à travers des pointeurs ou des références.

0 votes

Merci. Je ne sais pas pourquoi j'ai raté ce cas évident. Je ne vois toujours pas pourquoi la fonction devrait être obsolète, mais au moins la décision me semble plus logique maintenant.

0 votes

Je viens de mettre à jour ma question un peu. Si vous avez des idé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 signifié que d'autres threads n'ont pas accès à s.

8voto

yohjp Points 1036

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

int main() {
  int result = 0;
  auto sp1 = std::make_shared(0);  // refcount: 1

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

  // Faire des tâches en multithreading:
  //   D'autres threads peuvent incrémenter/décrémenter le refcount en même temps.

  if (sp1.unique()) {      // [U] refcount == 1?
    assert(result == 42);  // [R] lire 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 annexe: la terminaison du thread et la fonction membre join()
  // ont une relation de happens-before, donc [W] happens-before [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 happens-before de [D] destructeur de shared_ptr à [U] appelant unique(). Donc, nous ne pouvons pas attendre de relation [W] [D] [U] [R] et [W] [R]. ('' représente la relation de 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(). 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 indiquent dans leurs remarques que leur use_count() peut être inefficace. C'est une tentative de reconnaître les implémentations reflinked (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 reflinking, en particulier après que C++11 a reconnu l'existence de multithreading. Tout le monde utilise des refcounts atomiques, donc use_count() n'est qu'une charge atomique.

LWG2776 citation (soulignement de ma part) :

La suppression de la restriction "seulement pour le débogage" 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 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 un chargement relaxé, et ne fournissent pas cette garantie, car ce n'est pas spécifié dans la norme. Pour un usage en débogage/conseil, c'était OK. Sans cela, la spécification est peu claire et probablement trompeuse.

[...]

Je préférerais spécifier que use_count() fournit seulement un indice peu fiable du décompte réel (une autre façon de dire uniquement pour le débogage). Ou le déprécier, comme JF l'a suggéré. Nous ne pouvons pas rendre use_count() fiable sans ajouter beaucoup plus de cloisonnement. Nous ne voulons vraiment pas que quelqu'un attende que use_count() == 2 pour déterminer qu'un autre thread est arrivé aussi loin. Et malheureusement, je ne pense pas que nous disions actuellement quelque chose qui indique 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 être fouetté, mais il pourrait y avoir des schémas plus complexes où cette hypothèse est implicitement faite quelque part. J'aurais quand même préféré qu'ils corrigent unique() en précisant un effet de synchronisation au lieu de le rendre obsolète, mais il pourrait y avoir des implications en termes de performance 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 absolument pas 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 visionnement : http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0488r0.pdf

Ce document contient tous les commentaires des Organes Nationaux (NB) pour la réunion d'Issaquah. CA 14 se lit comme suit :

La suppression de la restriction "debug only" pour use_count() et unique() dans shared_ptr 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(). De nombreuses implémentations actuelles utilisent une charge détendue et ne fournissent pas cette garantie, car elle n'est pas spécifiée dans le Standard. Pour un usage de débogage/conseil, 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 être d'utiliser memory_order_acquire pour la fonction unique(), en spécifiant que les opérations de décrément du nombre de références se synchronisent avec unique()."

0 votes

@MikeMB Je crains que mes recherches n'aient rien révélé qui pourrait éclairer leur motivation. L'information peut exister, mais ce n'est pas un fruit facile à cueillir.

1 votes

La synchronisation nécessaire pour cela rendrait pessimiste certaines opérations assez courantes (comme la copie), afin de permettre un (ab-)usage de unique/use_count qui n'a jamais été prévu de toute façon.

1voto

Philippe Points 127

L'existence de std::enable_shared_from_this est ce qui pose problème pour utiliser de manière intéressante 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... Bien que cela ne concerne pas 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 : lorsque true, l'objet pointé peut être modifié sans effectuer une opération "copier-sur-écriture". 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 des "utilisations".

0 votes

Je ne comprends pas le lien. 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 exact, 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 différents threads, tant pour l'incrémenter que pour 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