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 sa dépréciation en C++17?

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

Cette fonction est obsolète à partir de C++17 Car use_count n'est qu'une approximation dans un environnement multi-thread.

je comprends que cela soit vrai pour use_count() > 1: pendant que je tiens une référence, quelqu'un d'autre pourrait simultanément lâcher la sienne ou créer une nouvelle copie.

Mais si use_count() renvoie 1 (ce qui m'intéresse lors de l'appel de unique()), Il n'y a pas d'autre thread qui pourrait changer cette valeur de manière racée, donc je m'attendrais à ce que cela soit sûr:

if (myPtr && myPtr.unique()) {
    //Modify *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 à C++17 CD comment CA 14 , mais je n'ai pas pu trouver dit le commentaire lui-même.

Comme alternative, ce document a proposé d'ajouter quelques notes, y compris les suivantes:

Remarque: Lorsque plusieurs threads peuvent affecter la valeur de retour de use_count(), le résultat doit être traité comme approximatif. en particulier, use_count() == 1 n'implique pas que les accès par un shared_ptr précédemment détruits ont en aucun cas terminé. - note de fin

Je comprends que cela pourrait être le cas pour la façon dont use_count() est actuellement spécifié (due à l'absence de synchronisation garantie), mais pourquoi la résolution n'a-t-elle pas seulement pour spécifier une telle synchronisation et donc rendre le modèle ci-dessus sûr? S'il y avait une limitation fondamentale qui ne permettrait pas une telle synchronisation (ou la rendrait prohibitivement coûteuse), alors 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 je n'ai utilisé que unique() sur les instances de {[10] } qui étaient non accessible aux autres threads eux-mêmes. Il n'y avait donc aucun danger que cette instance particulière soit copiée de manière racée.

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

À 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<std::array<uint8_t, 256>>();
        }
    }

    // the returned chunk of memory might be passed to a different thread(s),
    // but the function is never accessed from two threads at the same time
    std::shared_ptr<std::array<uint8_t,256>> getChunk()
    {
        auto it = std::find_if(_cache.begin(), _cache.end(), [](auto& ptr) { return ptr.unique(); });
        if (it != _cache.end()) {
            //memory is no longer used by previous user, so it can be given to someone else
            return *it;
        } else {
            return{};
        }
    }
private:
    std::vector<std::shared_ptr<std::array<uint8_t, 256>>> _cache;
};

Y a - t-il quelque chose de mal (Si unique() se synchronisait réellement avec les destructeurs d'autres copies)?

25
demandé sur Toby Speight 2016-12-14 15:10:41

3 réponses

Je pense que P0521R0 résout potentiellement course de données en abusant de shared_ptr comme synchronisation inter-thread. Il dit use_count() renvoie une valeur de refcount non fiable, et donc, unique() la fonction membre sera inutile lors du multithreading.

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

  // Start another thread
  std::thread another_thread([&result, sp2 = sp1]{  // refcount: 1 -> 2
    result = 42;  // [W] store to result
    // [D] expire sp2 scope, and refcount: 2 -> 1
  });

  // Do multithreading stuff:
  //   Other threads may concurrently increment/decrement refcounf.

  if (sp1.unique()) {      // [U] refcount == 1?
    assert(result == 42);  // [R] read from result
    // This [R] read action cause data race w.r.t [W] write action.
  }

  another_thread.join();
  // Side note: thread termination and join() member function
  // have happens-before relationship, so [W] happens-before [R]
  // and there is no data race on following read action.
  assert(result == 42);
}

La fonction membre unique() n'a aucun effet de synchronisation et il n'y a pas se produit-avant la relation du destructeur de [D] shared_ptr à [U] appelant unique(). Nous ne pouvons donc pas nous attendre à une relation [W] ⇒ [D] ⇒ [U] ⇒ [R] et [W] ⇒ [R]. ('⇒'indique se produit-avant la relation).


Édité: j'ai trouvé deux problèmes liés au LWG; 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 en C++1z.

Lwg2434 citation (emphase mine):

shared_ptr et weak_ptr avoir des Notes que leur use_count() pourrait être inefficace. ceci est une tentative de reconnaître les implémentations reflinked (qui peuvent être utilisées par les pointeurs Loki smart, par exemple). cependant, il n'y a pas d'implémentations shared_ptr qui utilisent reflinking, surtout après que C++11 ait reconnu L'existence du multithreading. Tout le monde utilise des refcounts atomiques, donc use_count() est juste une charge atomique .

Lwg2776 citation (emphase mine):

Le la suppression de la restriction "debug only" 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 synchronize pour s'assurer 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 une charge détendue, et ne fournissent pas cette garantie, car elle n'est pas indiquée dans la norme. pour l'utilisation de débogage / indice qui était OK. Sans elle la spécification est peu clair et probablement trompeur.

[...]

Je préférerais spécifier use_count() comme ne fournissant qu'un indice peu fiable du nombre réel (une autre façon de dire déboguer seulement). Ou le déprécier, comme JF l'a suggéré. nous ne pouvons pas rendre use_count() fiable sans ajouter beaucoup plus de clôtures. Nous ne voulons vraiment pas que quelqu'un attende use_count() == 2 pour déterminer qu'un autre thread est allé aussi loin. Et malheureusement, je ne pense pas que nous disons actuellement quoi que ce soit pour préciser que c'est un erreur.

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

7
répondu yohjp 2016-12-16 01:41:30

Considérons le code suivant:

// global variable
std::shared_ptr<int> s = std::make_shared<int>();

// thread 1
if (s && s.unique()) {
    // modify *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 tandis 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 les autres threads n'ont pas accès à shared_ptr initial directement ou via des pointeurs ou des références.

12
répondu alexeykuzmin0 2016-12-14 12:20:36

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

Ce document contient tous les commentaires du NB (organisme National) 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 introduit un bug: pour unique () à produire une valeur utile et fiable, il a besoin d'une clause de synchronisation pour assurez - vous que les accès antérieurs via une autre référence sont visible à l'appelant réussi de unique (). De nombreuses implémentations actuelles utilisent un charge détendue, et ne fournissent pas cette garantie, car il n'est pas indiqué dans la Norme. Pour l'utilisation de débogage/indice qui était OK. Sans elle, l' la spécification n'est pas claire et trompeuse.

3
répondu Mikel F 2016-12-14 18:01:36