2 votes

std::shared_ptr Thread Safe

L'utilisation d'un pointeur partagé (std::shared_ptr) est-elle sûre dans un programme multithread ?
Je ne considère pas les accès en lecture/écriture aux données appartenant au pointeur partagé mais plutôt le pointeur partagé lui-même.

Je suis conscient que certaines implémentations (comme MSDN) fournissent cette garantie supplémentaire, mais je veux savoir si elle est garantie par la norme et si elle est donc portable.

#include <thread>
#include <memory>
#include <iostream>

void function_to_run_thread(std::shared_ptr<int> x)
{
    std::cout << x << "\n";
}
// Shared pointer goes out of scope.
// Is its destruction here guaranteed to happen only once?
// Or is this a "Data Race" situation that is UB?

int main()
{
    std::thread   threads[2];

    {
        // A new scope
        // So that the shared_ptr in this scope has the
        // potential to go out of scope before the threads have executed.
        // So leaving the shared_ptr in the scope of the threads only.
        std::shared_ptr<int>   data = std::make_shared<int>(5);

        // Create workers.
        threads[0] = std::thread(function_to_run_thread, data);
        threads[1] = std::thread(function_to_run_thread, data);
    }
    threads[0].join();
    threads[1].join();
}

Tout lien vers des sections de la norme est le bienvenu.

Je serais heureux que les gens fassent référence aux principales implémentations afin que nous puissions considérer que le système est portable pour la plupart des développeurs normaux.

  • MSDN : Vérifier. Thread Safe.
  • G++ : ?
  • clang : ?

Je considère qu'il s'agit là des principales mises en œuvre, mais je suis prêt à en envisager d'autres.

1voto

JVApen Points 4523

Je n'ai pas de liens vers la norme. Je l'ai vérifiée il y a longtemps, std::shared_ptr est thread-safe sous certaines conditions, qui se résument à : chaque thread doit avoir sa propre copie. Comme documenté sur Référence cpp :

Toutes les fonctions membres (y compris le constructeur de copie et l'affectation de copie) peuvent être appelées par plusieurs threads sur différentes instances de shared_ptr sans synchronisation supplémentaire, même si ces instances sont des copies et partagent la propriété du même objet. Si plusieurs threads d'exécution accèdent au même shared_ptr sans synchronisation et que l'un de ces accès utilise une fonction membre non-const de shared_ptr, une course aux données se produira.

Ainsi, comme pour toute autre classe de la norme, la lecture de la même instance à partir de plusieurs threads est autorisée. L'écriture dans cette instance à partir d'un seul thread ne l'est pas.

int main()
{
    std::vector<std::thread>   threads;

    {
        // A new scope
        // So that the shared_ptr in this scope has the
        // potential to go out of scope before the threads have executed.
        // So leaving the shared_ptr in the scope of the threads only.
        std::shared_ptr<int>   data = std::make_shared<int>(5);

        // Perfectly legal to read access the shared_ptr
        threads.emplace_back(std::thread([&data]{ std::cout << data.get() << '\n'; }));        
        threads.emplace_back(std::thread([&data]{ std::cout << data.get() << '\n'; }));

        // This line will result in a race condition as you now have read and write on the same instance
        threads.emplace_back(std::thread([&data]{ data = std::make_shared<int>(42); }));

        for (auto &thread : threads)
           thread.join();
    }
}

Une fois que nous avons affaire à des copies multiples du shared_ptr, tout va bien :

int main()
{
    std::vector<std::thread>   threads;

    {
        // A new scope
        // So that the shared_ptr in this scope has the
        // potential to go out of scope before the threads have executed.
        // So leaving the shared_ptr in the scope of the threads only.
        std::shared_ptr<int>   data = std::make_shared<int>(5);

        // Perfectly legal to read access the shared_ptr copy
        threads.emplace_back(std::thread([data]{ std::cout << data.get() << '\n'; }));        
        threads.emplace_back(std::thread([data]{ std::cout << data.get() << '\n'; }));

        // This line will no longer result in a race condition the other threads are using a copy
        threads.emplace_back(std::thread([&data]{ data = std::make_shared<int>(42); }));

        for (auto &thread : threads)
           thread.join();
    }
}

La destruction du shared_ptr se fera également sans problème, car chaque thread appellera le destructeur du shared_ptr local et le dernier nettoiera les données. Il y a quelques opérations atomiques sur le compte de référence pour s'assurer que cela se passe correctement.

int main()
{
    std::vector<std::thread>   threads;

    {
        // A new scope
        // So that the shared_ptr in this scope has the
        // potential to go out of scope before the threads have executed.
        // So leaving the shared_ptr in the scope of the threads only.
        std::shared_ptr<int>   data = std::make_shared<int>(5);

        // Perfectly legal to read access the shared_ptr copy
        threads.emplace_back(std::thread([data]{ std::cout << data.get() << '\n'; }));        
        threads.emplace_back(std::thread([data]{ std::cout << data.get() << '\n'; }));

        // Sleep to ensure we have some delay
        threads.emplace_back(std::thread([data]{ std::this_thread::sleep_for(std::chrono::seconds{2}); }));
    }
    for (auto &thread : threads)
       thread.join();
}

Comme vous l'avez déjà indiqué, l'accès aux données dans le shared_ptr n'est pas protégé. Donc, comme dans le premier cas, si vous avez un thread qui lit et un thread qui écrit, vous avez toujours un problème. Cela peut être résolu avec des atomiques ou des mutex ou en garantissant la lecture seule des objets.

1voto

hoffmale Points 213

En citant le dernière version :

Afin de déterminer la présence d'une course aux données, les fonctions membres ne doivent accéder et modifier que les éléments suivants shared_ptr y weak_ptr les objets eux-mêmes et non les objets auxquels ils font référence. Les changements dans use_count() ne reflètent pas les modifications qui peuvent introduire des courses de données.

Il y a donc beaucoup de choses à assimiler. La première phrase parle des fonctions membres qui n'accèdent pas au pointeur, c'est-à-dire que l'accès au pointeur n'est pas thread-safe.

Mais il y a aussi la deuxième phrase. Effectivement, cela force toute opération qui changerait use_count() (par exemple, la construction d'une copie, l'affectation, la destruction, l'appel reset ) pour qu'elles soient sûres pour les fils, mais seulement dans la mesure où elles affectent les use_count() .

Ce qui est logique : Des fils différents copiant le même std::shared_ptr (ou la destruction de la même std::shared_ptr ) ne doit pas provoquer une course aux données concernant la propriété de la pointe. La valeur interne de use_count() doivent être synchronisés.

J'ai vérifié, et cette formulation exacte était également présente dans N3337 section 20.7.2.2, paragraphe 4, on peut donc affirmer sans risque de se tromper que cette exigence existe depuis l'introduction de l'euro. std::shared_ptr dans C++11 (et n'a pas été introduit plus tard).

0voto

Shine Points 93

Shared_ptr (et aussi weak_ptr) utilise des entiers atomiques pour garder le compte d'utilisation, donc le partage entre threads est sûr mais bien sûr, l'accès aux données nécessite toujours des mutex ou toute autre synchronisation.

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