web-dev-qa-db-fra.com

std :: shared_ptr thread safety

J'ai lu ça

"Plusieurs threads peuvent simultanément lire et écrire différents objets shared_ptr, même lorsque les objets sont des copies qui partagent la propriété." ( MSDN: Thread Safety dans la bibliothèque C++ standard )

Cela signifie-t-il que la modification de l'objet shared_ptr est sûre?
Par exemple, le code suivant est-il considéré comme sûr:

shared_ptr<myClass> global = make_shared<myClass>();
...

//In thread 1
shared_ptr<myClass> private = global;
...

//In thread 2
global = make_shared<myClass>();
...

Puis-je être sûr dans ce cas que le thread 1 private aura la valeur d'origine de global ou la nouvelle valeur affectée par le thread 2, mais de toute façon, il aura un shared_ptr valide pour myClass?

== EDIT ==
Juste pour expliquer ma motivation. Je veux avoir un pointeur partagé pour conserver ma configuration et j'ai un pool de threads pour gérer les demandes.
donc global est la configuration globale.
thread 1 prend la configuration actuelle alors qu'il commence à traiter une demande.
thread 2 met à jour la configuration. (ne s'applique qu'aux futures demandes)

Si cela fonctionne, je peux mettre à jour la configuration de cette manière sans la casser au milieu d'un traitement de demande.

43
Roee Gavirel

Ce que vous lisez ne signifie pas ce que vous pensez que cela signifie. Tout d'abord, essayez la page msdn pour shared_ptr lui-même.

Faites défiler vers le bas dans la section "Remarques" et vous arriverez à la chair du problème. Fondamentalement, un shared_ptr<> pointe vers un "bloc de contrôle", c'est-à-dire comment il garde la trace du nombre shared_ptr<> les objets pointent réellement vers l'objet "Réel". Donc, quand vous faites cela:

shared_ptr<int> ptr1 = make_shared<int>();

Bien qu'il n'y ait qu'un seul appel pour allouer de la mémoire ici via make_shared, il y a deux blocs "logiques" que vous ne devriez pas traiter de la même façon. L'un est le int qui stocke la valeur réelle, et l'autre est le bloc de contrôle, qui stocke tous les shared_ptr<> "magie" qui le fait fonctionner.

Seul le bloc de contrôle lui-même est thread-safe.

Je mets cela sur sa propre ligne pour souligner. Le contenu du shared_ptr ne sont pas thread-safe, ni écrire sur le même shared_ptr exemple. Voici quelque chose pour démontrer ce que je veux dire:

// In main()
shared_ptr<myClass> global_instance = make_shared<myClass>();
// (launch all other threads AFTER global_instance is fully constructed)

//In thread 1
shared_ptr<myClass> local_instance = global_instance;

C'est très bien, en fait, vous pouvez le faire dans tous les threads autant que vous le souhaitez. Et puis quand local_instance est détruit (en sortant de la portée), il est également thread-safe. Quelqu'un peut accéder à global_instance et cela ne fera aucune différence. L'extrait que vous avez extrait de msdn signifie "l'accès au bloc de contrôle est thread-safe", donc les autres shared_ptr<> les instances peuvent être créées et détruites sur différents threads autant que nécessaire.

//In thread 1
local_instance = make_shared<myClass>();

C'est bon. Il sera affecte le global_instance objet, mais seulement indirectement. Le bloc de contrôle vers lequel il pointe sera décrémenté, mais effectué de manière thread-safe. local_instance ne pointera plus vers le même objet (ou bloc de contrôle) que global_instance Est-ce que.

//In thread 2
global_instance = make_shared<myClass>();

Ce n'est certainement pas très bien si global_instance est accessible à partir de tous les autres threads (ce que vous dites que vous faites). Il a besoin d'un verrou si vous faites cela parce que vous écrivez partout où global_instance vit, pas seulement en lisant. Donc, écrire sur un objet à partir de plusieurs threads est mauvais, sauf si vous l'avez protégé par un verrou. Vous pouvez donc lire à partir de global_instance l'objet en attribuant un nouveau shared_ptr<> des objets mais vous ne pouvez pas y écrire.

// In thread 3
*global_instance = 3;
int a = *global_instance;

// In thread 4
*global_instance = 7;

La valeur de a n'est pas définie. Ce pourrait être 7, ou ce pourrait être 3, ou ce pourrait être autre chose aussi. La sécurité des fils du shared_ptr<> les instances s'appliquent uniquement à la gestion de shared_ptr<> instances qui ont été initialisées les unes des autres, pas ce vers quoi elles pointent.

Pour souligner ce que je veux dire, regardez ceci:

shared_ptr<int> global_instance = make_shared<int>(0);

void thread_fcn();

int main(int argc, char** argv)
{
    thread thread1(thread_fcn);
    thread thread2(thread_fcn);
    ...
    thread thread10(thread_fcn);

    chrono::milliseconds duration(10000);
    this_thread::sleep_for(duration);

    return;
}

void thread_fcn()
{
    // This is thread-safe and will work fine, though it's useless.  Many
    // short-lived pointers will be created and destroyed.
    for(int i = 0; i < 10000; i++)
    {
        shared_ptr<int> temp = global_instance;
    }

    // This is not thread-safe.  While all the threads are the same, the
    // "final" value of this is almost certainly NOT going to be
    // number_of_threads*10000 = 100,000.  It'll be something else.
    for(int i = 0; i < 10000; i++)
    {
        *global_instance = *global_instance + 1;
    }
}

UNE shared_ptr<> est un mécanisme garantissant que plusieurs objets propriétaires garantissent la destruction d'un objet, et non un mécanisme permettant de garantir que plusieurs threads peuvent accéder correctement à un objet. Vous avez toujours besoin d'un mécanisme de synchronisation distinct pour l'utiliser en toute sécurité dans plusieurs threads (comme std :: mutex ).

La meilleure façon d'y penser à l'OMI est que shared_ptr<> s'assure que plusieurs copies pointant vers la même mémoire n'ont pas de problèmes de synchronisation pour lui-même, mais ne font rien pour l'objet pointé. Traitez-le comme ça.

85
Kevin Anderson

Pour ajouter à ce que Kevin a écrit, la spécification C++ 14 a un support supplémentaire pour l'accès atomique aux objets shared_ptr eux-mêmes:

20.8.2.6 shared_ptr accès atomique [util.smartptr.shared.atomic]

Accès simultané à un shared_ptr L'objet de plusieurs threads n'introduit pas de course aux données si l'accès se fait exclusivement via les fonctions de cette section et que l'instance est passée comme premier argument.

Donc si vous le faites:

//In thread 1
shared_ptr<myClass> private = atomic_load(&global);
...

//In thread 2
atomic_store(&global, make_shared<myClass>());
...

ce sera thread-safe.

23
Chris Dodd

Cela signifie que vous aurez un shared_ptr, et un comptage de références valide.

Vous décrivez une condition de concurrence critique entre 2 threads qui tentent de lire/affecter à la même variable.

Parce que c'est un comportement indéfini en général (cela n'a de sens que dans le contexte et le calendrier du programme individuel) shared_ptr ne gère pas cela.

4
Yochai Timmer

Les opérations de lecture ne sont pas sujettes à des courses de données entre elles, il est donc sûr de partager la même instance de shared_ptr entre les threads tant que tous les threads utilisent méthodes const uniquement (cela inclut la création de copies). Dès qu'un thread utilise une méthode non const (comme dans "pointer vers un autre objet"), cette utilisation n'est plus thread-safe.

L'exemple OP n'est pas thread-safe et nécessiterait l'utilisation de la charge atomique dans le thread 1. et du magasin atomique dans le thread 2 (section 2.7.2.5 en C++ 11) pour le rendre thread-safe.

Le mot clé dans le texte MSDN est en effet différents objets shared_ptr, comme déjà indiqué dans les réponses précédentes.

2
Leon

Je pense que les réponses à ce jour à cette question sont trompeuses en ce qui concerne le scénario décrit. J'ai un scénario très similaire décrit dans la question. Tous les autres threads ont (besoin) juste un accès en lecture seule à la configuration actuelle qui est obtenue par:

// In thread n
shared_ptr<MyConfig> sp_local = sp_global;

Aucun de ces threads ne va modifier le contenu de l'objet MyConfig. Nombre de références pour sp_global est incrémenté pour chaque exécution de la ligne ci-dessus.

Thread 1, réinitialise périodiquement le sp_global à une autre instance de la configuration:

// In thread 1
shared_ptr<MyConfig> sp_global = make_shared<MyConfig>(new MyConfig);

Cela devrait également être sûr. Il définit le nombre de références de sp_global retour à 1, et le sp_global pointe maintenant vers la dernière configuration, comme pour toutes les nouvelles copies locales. Donc, si je ne manque rien ici, tout cela devrait être totalement sûr pour les threads.

#include <iostream>
#include <memory>

using namespace std;

shared_ptr<int> sp1(new int(10));

int main()
{
    cout<<"Hello World! \n";

    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "---------\n";

    shared_ptr<int> sp2 = sp1;
    shared_ptr<int>* psp3 = new shared_ptr<int>;
    *psp3 = sp1;
    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
    cout << "---------\n";

    sp1.reset(new int(20));

    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
    cout << "---------\n";

    delete psp3;
    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "---------\n";

    sp1 = nullptr;

    cout << "sp1 use count: " << sp1.use_count() << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";

    return 0;
}

et la sortie

Hello World!
sp1 use count: 1, sp1: 10
---------
sp1 use count: 3, sp1: 10
sp2 use count: 3, sp2: 10
sp3 use count: 3, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 2, sp2: 10
sp3 use count: 2, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 1, sp2: 10
---------
sp1 use count: 0
sp2 use count: 1, sp2: 10
1
hagh

voici ma compréhension de la sécurité des threads de shared_ptr. OMI, il y a trois aspects en matière de sécurité des threads de shared_ptr.

Le premier est shared_ptr lui-même. Je dirais que shared_ptr lui-même n'est pas sûr pour les threads, ce qui signifie qu'il y a une course aux données lorsque nous essayons d'accéder à un objet shared_ptr dans plusieurs threads et l'un des accès écrit. Par exemple, nous avons une course aux données dans la situation suivante:

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
global_ptr.reset();

Le deuxième aspect est la structure interne de shared_ptr. Je dirais que c'est thread-safe. Le résultat est qu'il n'y a pas de course de données lors de l'accès à plusieurs objets shared_ptr et les objets pointent vers le même objet géré. Par exemple, nous n'avons pas de course aux données dans la situation suivante:

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
shared_ptr<string> local_ptr = global_ptr;
local_ptr.reset();

Le troisième aspect est que l'objet géré dans le shared_ptr peut ou non être thread-safe. Par exemple, je dirais qu'il y a une course aux données dans la situation suivante:

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
shared_ptr<string> local_ptr = global_ptr;
(*local_ptr).clear();

https://gcc.gnu.org/onlinedocs/libstdc++/manual/memory.html#shared_ptr.thread

https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic

0
Lujun Weng