faible_ptr en singleton non thread-safe

J'écris une fonction qui renvoie un shared_ptr à un singleton. Je veux que l'objet singleton soit détruit lorsque toutes les références ont disparu. Ma solution s'appuie sur cette réponse acceptée qui utilise un statique weak_ptret mutex, mais mon test de sécurité des threads ne réussit pas de manière cohérente.

Voici un exemple complet qui illustre le problème.

#include <gtest/gtest.h>
#include <atomic>
#include <mutex>
#include <memory>
#include <vector>
#include <thread>
using namespace std;
// Number of instances of this class are tracked in a static counter.
// Used to verify the get_singleton logic (below) is correct.
class CountedObject {
private:
static atomic<int> instance_counter;
public:
CountedObject() {
int prev_counter = instance_counter.fetch_add(1);
if (prev_counter!= 0)
// Somehow, 2 objects exist at the same time. Why?
throw runtime_error("Constructed " + to_string(prev_counter + 1) +
" counted objects");
}
~CountedObject() {
instance_counter.fetch_sub(1);
}
static int count() {
return instance_counter.load();
}
};
atomic<int> CountedObject::instance_counter{0};
// Returns reference to a singleton that gets destroyed when all references
// are destroyed.
template <typename T>
std::shared_ptr<T> get_singleton() {
static mutex mtx;
static weak_ptr<T> weak;
scoped_lock lk(mtx);
shared_ptr<T> shared = weak.lock();
if (!shared) {
shared.reset(new T, [](T* ptr){
scoped_lock lk(mtx);
delete ptr;
});
weak = shared;
}
return shared;
}
// This test passes consistently.
TEST(GetSingletonTest, SingleThreaded) {
ASSERT_EQ(CountedObject::count(), 0);
auto ref1 = get_singleton<CountedObject>();
auto ref2 = get_singleton<CountedObject>();
ASSERT_EQ(CountedObject::count(), 1);
ref1.reset();
ASSERT_EQ(CountedObject::count(), 1);
ref2.reset();
ASSERT_EQ(CountedObject::count(), 0);
}
// This test does NOT pass consistently.
TEST(GetSingletonTest, MultiThreaded) {
const int THREAD_COUNT = 2;
const int ITERS = 1000;
vector<thread> threads;
for (int i = 0; i < THREAD_COUNT; ++i)
threads.emplace_back([ITERS]{
// Repeatedly obtain and release references to the singleton.
// The invariant must hold that at most one instance ever exists
// at a time.
for (int j = 0; j < ITERS; ++j) {
auto local_ref = get_singleton<CountedObject>();
local_ref.reset();
}
});
for (auto& t: threads)
t.join();
}

Sur mon système (ARM64 Linux, g++ 7.5.0), le test multithread échoue généralement :

[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from GetSingletonTest
[ RUN ] GetSingletonTest.SingleThreaded
[ OK ] GetSingletonTest.SingleThreaded (0 ms)
[ RUN ] GetSingletonTest.MultiThreaded
terminate called after throwing an instance of 'std::runtime_error'
what(): Constructed 2 counted objects
Aborted (core dumped)

J'ai omis coutles messages du code par souci de brièveté, mais séparément j'ai ajouté des messages pour déboguer ce qui se passe :

[thread id, message]
...
547921465808 Acquired lock
547921465808 Weak ptr expired, constructing
547921465808 Releasing lock
547929858512 Acquired lock
547929858512 Weak ptr expired, constructing
terminate called after throwing an instance of 'std::runtime_error'
what(): Constructed 2 counted objects
Aborted (core dumped)

Il semble que le thread 1 détermine que le singleton a expiré et le reconstruit. Ensuite, le thread 2 se réveille et détermine également que le singleton a expiré, même si le thread 1 vient de le repeupler - comme si le thread 2 fonctionnait avec une version "obsolète" du pointeur faible.

Comment puis-je rendre l'affectation à weakpartir du fil 1 immédiatement visible pour le fil 2 ? Cela peut-il être réalisé avec C++20 atomic<weak_ptr<T>>? Mon projet est limité à C++17, donc malheureusement ce n'est pas une option pour moi.

J'apprécie ton aide!


Solution du problème

L'affectation sous verrou sera visible par tout autre thread vérifiant le weak_ptrsous verrou.

La raison pour laquelle plusieurs instances existent est que l'ordre des opérations du destructeur ne garantit pas le contraire. !weak.lock()n'est pas équivalent à "avec certitude, il n'y a pas d'instances de cet objet". Cela équivaut à l'inverse de "avec certitude, il existe une instance de cet objet". Il peut y avoir une instance, car le décompte de références est décrémenté avant que l'effaceur n'ait la possibilité d'agir.

Imaginez cette séquence:


  • Thread 1: ~shared_ptrest appelé.

  • Thread 1 : Le nombre de références fortes est décrémenté.

  • Thread 1: Il compare égal à zéro et le suppresseur commence.

  • Thread 2: get_singletonest appelé.

  • Thread 2: Le verrou est acquis.

  • Thread 2: !weak.lock()est satisfait, nous construisons donc une nouvelle instance.

  • Thread 1:... en attendant, nous attendons d'acquérir la serrure...

  • Thread 2 : la nouvelle instance est construite et assignée, et le verrou est libéré.

  • Thread 1 : Maintenant, le suppresseur peut acquérir le verrou et supprimer l'instance d'origine.

Commentaires

Posts les plus consultés de ce blog

Erreur Symfony : "Une exception a été levée lors du rendu d'un modèle"

Détecter les appuis sur les touches fléchées en JavaScript

Une chaîne vide donne "Des erreurs ont été détectées dans les arguments de la ligne de commande, veuillez vous assurer que tous les arguments sont correctement définis"