web-dev-qa-db-fra.com

Quelle est la différence entre std :: shared_ptr et std :: experimental :: atomic_shared_ptr?

J'ai lu l'article suivant par Antony Williams et comme je l'ai compris en plus du nombre atomique partagé dans std::shared_ptr dans std::experimental::atomic_shared_ptr le pointeur réel sur l'objet partagé est également atomique?

Mais quand j'ai lu la version comptée par référence de lock_free_stack décrit dans le livre d'Antony sur C++ Concurrency il me semble que la même chose s'applique aussi pour std::shared_ptr, car des fonctions comme std::atomic_load, std::atomic_compare_exchnage_weak sont appliqués aux instances de std::shared_ptr.

template <class T>
class lock_free_stack
{
public:
  void Push(const T& data)
  {
    const std::shared_ptr<node> new_node = std::make_shared<node>(data);
    new_node->next = std::atomic_load(&head_);
    while (!std::atomic_compare_exchange_weak(&head_, &new_node->next, new_node));
  }

  std::shared_ptr<T> pop()
  {
    std::shared_ptr<node> old_head = std::atomic_load(&head_);
    while(old_head &&
          !std::atomic_compare_exchange_weak(&head_, &old_head, old_head->next));
    return old_head ? old_head->data : std::shared_ptr<T>();
  }

private:
  struct node
  {
    std::shared_ptr<T> data;
    std::shared_ptr<node> next;

    node(const T& data_) : data(std::make_shared<T>(data_)) {}
  };

private:
  std::shared_ptr<node> head_;
};

Quelle est la différence exacte entre ces deux types de pointeurs intelligents et si le pointeur dans std::shared_ptr l'instance n'est pas atomique, pourquoi est-il possible que l'implémentation de pile sans verrou ci-dessus soit possible?

22
bobeff

La "chose" atomique dans shared_ptr n'est pas le pointeur partagé lui-même, mais le bloc de contrôle vers lequel il pointe. ce qui signifie que tant que vous ne modifiez pas le shared_ptr sur plusieurs threads, ça va. notez que copie a shared_ptr mute uniquement le bloc de contrôle, et non le shared_ptr lui-même.

std::shared_ptr<int> ptr = std::make_shared<int>(4);
for (auto i =0;i<10;i++){
   std::thread([ptr]{ auto copy = ptr; }).detach(); //ok, only mutates the control block 
}

La mutation du pointeur partagé lui-même, comme l'affectation de valeurs différentes à plusieurs threads, est une course aux données, par exemple:

std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::thread threadA([&ptr]{
   ptr = std::make_shared<int>(10);
});
std::thread threadB([&ptr]{
   ptr = std::make_shared<int>(20);
});    

Ici, nous mutons le bloc de contrôle (ce qui est correct) mais aussi le pointeur partagé lui-même, en le faisant pointer vers des valeurs différentes de plusieurs threads. Ce n'est pas ok.

Une solution à ce problème consiste à envelopper le shared_ptr avec un verrou, mais cette solution n'est pas si évolutive dans certains cas, et dans un sens, perd la sensation automatique du pointeur partagé standard.

Une autre solution consiste à utiliser les fonctions standard que vous avez citées, telles que std::atomic_compare_exchange_weak. Cela rend le travail de synchronisation des pointeurs partagés manuel, ce que nous n'aimons pas.

C'est là que le pointeur partagé atomique vient jouer. Vous pouvez muter le pointeur partagé de plusieurs threads sans craindre une course aux données et sans utiliser de verrous. Les fonctions autonomes seront celles des membres, et leur utilisation sera beaucoup plus naturelle pour l'utilisateur. Ce type de pointeur est extrêmement utile pour les structures de données sans verrouillage.

23
David Haim

N4162(pdf), la proposition de pointeurs intelligents atomiques, a une bonne explication. Voici une citation de la partie pertinente:

Cohérence . Pour autant que je sache, les fonctions [util.smartptr.shared.atomic] sont les seules opérations atomiques de la norme qui ne sont pas disponibles via un type atomic. Et pour tous les types en plus de shared_ptr, Nous enseignons aux programmeurs à utiliser des types atomiques en C++, pas des fonctions de style C atomic_*. Et c'est en partie à cause de ...

Exactitude . L'utilisation des fonctions gratuites rend le code sujet aux erreurs et racé par défaut. Il est de loin supérieur d'écrire atomic une fois sur la déclaration de variable elle-même et de savoir que tous les accès seront atomiques, au lieu de devoir se rappeler d'utiliser l'opération atomic_* Sur tous utilisation de l'objet, même des lectures apparemment simples. Ce dernier style est sujet aux erreurs; par exemple, "mal faire" signifie simplement écrire des espaces blancs (par exemple, head au lieu de atomic_load(&head)), de sorte que dans ce style, chaque utilisation de la variable est "mauvaise par défaut". Si vous oubliez d'écrire l'appel atomic_* À un seul endroit, votre code continuera à être compilé avec succès sans erreurs ni avertissements, il "semblera fonctionner", y compris probablement la plupart des tests, mais contiendra toujours une course silencieuse avec un comportement indéfini qui apparaît généralement sous la forme de défaillances intermittentes difficiles à reproduire, souvent/généralement sur le terrain, et je m'attends également à des vulnérabilités exploitables dans certains cas. Ces classes d'erreurs sont éliminées en déclarant simplement la variable atomic, car alors c'est sûr par défaut et pour écrire le même ensemble de bogues, il faut du code explicite non blanc (parfois des arguments explicites memory_order_*, Et généralement reinterpret_cast ing).

Performances . atomic_shared_ptr<> En tant que type distinct a un avantage d'efficacité important sur les fonctions de [util.smartptr.shared.atomic] - il peut simplement stocker un atomic_flag Supplémentaire (ou similaire) pour le verrou tournant interne comme habituel pour atomic<bigstruct>. En revanche, les fonctions autonomes existantes doivent être utilisables sur tout objet shared_ptr Arbitraire, même si la grande majorité des shared_ptr Ne seront jamais utilisées de manière atomique. Cela rend les fonctions libres intrinsèquement moins efficaces; par exemple, l'implémentation peut nécessiter que chaque shared_ptr transporte le surdébit d'une variable spinlock interne (meilleure concurrence, mais surdébit significatif par shared_ptr), sinon la bibliothèque doit conserver une structure de données de côté pour stocker les informations supplémentaires pour shared_ptr s qui sont réellement utilisés de manière atomique, ou (pire et apparemment courant dans la pratique), la bibliothèque doit utiliser un verrou tournant global.

6
cpplearner

Appeler std::atomic_load() ou std::atomic_compare_exchange_weak() sur un shared_ptr Est fonctionnellement équivalent à appeler atomic_shared_ptr::load() ou atomic_shared_ptr::atomic_compare_exchange_weak(). Il ne devrait pas y avoir de différence de performances entre les deux. L'appel de std::atomic_load() ou std::atomic_compare_exchange_weak() sur un atomic_shared_ptr Serait redondant syntaxiquement et pourrait ou non entraîner une baisse des performances.

5
atb

atomic_shared_ptr est un raffinement d'API. shared_ptr prend déjà en charge les opérations atomiques, mais uniquement lors de l'utilisation des fonctions atomiques non membres . Ceci est sujet aux erreurs, car les opérations non atomiques restent disponibles et sont trop faciles à invoquer par accident pour un programmeur imprudent. atomic_shared_ptr est moins sujet aux erreurs car il n'expose aucune opération non atomique.

shared_ptr et atomic_shared_ptr expose différentes API, mais elles n'ont pas nécessairement besoin d'être implémentées différemment; shared_ptr prend déjà en charge toutes les opérations exposées par atomic_shared_ptr. Cela dit, les opérations atomiques de shared_ptr n'est pas aussi efficace qu'il pourrait l'être, car il doit également prendre en charge les opérations non atomiques. Il existe donc des raisons de performances pour lesquelles atomic_shared_ptr pourrait être implémenté différemment. Cela est lié au principe de responsabilité unique. "Une entité avec plusieurs objectifs disparates ... offre souvent des interfaces paralysées pour l'un de ses objectifs spécifiques, car le chevauchement partiel entre les différents domaines de fonctionnalité brouille la vision nécessaire pour une implémentation précise de chacun." (Sutter & Alexandrescu 2005, Normes de codage C++ )

4
Oktalist