feek_ptr到singleton而不是线程安全
我正在编写将共享_ptr返回单例的函数。我希望当所有参考文献消失时,单身对象都会被摧毁。我的解决方案以使用静态feek_ptr
和mutex ,但是我对其线程安全的测试并不始终如一。
这是一个说明问题的完整示例。
#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();
}
在我的系统(ARM64 Linux,g ++ 7.5.0)上,多线程测试通常会失败:
[==========] 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)
我省略了cout
来自代码的消息,但分别添加了消息以调试什么是发生:
[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)
看来线程1确定单例已过期并重新构造它。然后螺纹2醒来, 也确定了单例已经过期,即使线程1刚刚填充了它 - 好像线程2正在使用弱的“过时”版本指针。
如何将分配从线程1立即可见到线程2的弱
?这可以通过C ++ 20的atomic&lt; feal_ptr&lt; t&gt;&gt;
来实现吗?我的项目仅限于C ++ 17,因此不幸的是,这对我来说不是一个选择。
感谢您的帮助!
I'm writing a function that returns a shared_ptr to a singleton. I want the singleton object to be destroyed when all of the references have gone away. My solution builds on this accepted answer which uses a static weak_ptr
and mutex
, but my test for its thread-safety does not pass consistently.
Here is a complete example that illustrates the problem.
#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();
}
On my system (ARM64 Linux, g++ 7.5.0), the multi-threaded test usually fails:
[==========] 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)
I've omitted cout
messages from the code for brevity, but separately I've added messages to debug what's happening:
[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)
It appears that thread 1 determines the singleton has expired and re-constructs it. Then thread 2 wakes up and also determines the singleton has expired, even though thread 1 has just re-populated it - as if thread 2 was working with an "out-of-date" version of the weak pointer.
How can I make the assignment to weak
from thread 1 immediately visible to thread 2? Can this be achieved with C++20's atomic<weak_ptr<T>>
? My project is restricted to C++17, so unfortunately that is not an option for me.
I appreciate your help!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
锁定下的分配将可见到任何其他线程检查
feek_ptr
锁定下的分配。存在多个实例的原因是驱动器的操作顺序不能保证否则。
!它等同于“有确定性,有一个对象的实例”的倒数。可能会有一个实例,因为在eleter有机会采取行动之前,参考计数已减少。
想象一下此序列:
〜共享_ptr
被调用。get_singleton
被调用。!
编辑:
为此,您需要一些指示没有实例。只有在实例被销毁之后才能重置。其次,您需要决定如果遇到存在实例但正在破坏实例的情况下该怎么办:
要说以不同的方式,状态机器中的某些状态:
bool,让我们知道我们处于州#4。一种方法是循环,直到我们不再处于州#4为止。我们放下锁,使删除器可以取得进展。当我们重新锁定锁定时,另一个线程可能已经涌入并创建了实例,因此我们需要从顶部开始并重新检查所有内容。
The assignment under lock will be visible to any other thread checking the
weak_ptr
under lock.The reason multiple instances exist is that the destructor order of operations doesn't guarantee otherwise.
!weak.lock()
isn't equivalent to "with certainty, there are no instances of this object". It is equivalent to the inverse of "with certainty, there is an instance of this object". There may be an instance, because the reference count is decremented before the deleter has a chance to act.Imagine this sequence:
~shared_ptr
is called.get_singleton
is called.!weak.lock()
is satisfied, so we construct a new instance.EDIT:
To accomplish this, you need some indicator that there is no instance. That must be reset only after the instance is destroyed. And second, you need to decide what to do if you encounter this case where an instance exists but is being destroyed:
To put it a different way, there are these states in the state machine:
The bool lets us know that we are in state #4. One approach is to loop until we are no longer in state #4. We drop the lock so that deleters can make progress. When we retake the lock, another thread may have swooped in and created the instance, so we need to start at the top and re-check everything.
非常感谢Jeff的解释和回答。我正在接受他的答案,但我想发布一个替代实现,在我的(公认的)测试中,表现稍好一些。您的里程可能会有所不同。
这可以从eleter中删除锁定的采集,并用原子标志代替,该原子标志在对象实际删除时变为真。
Many thanks to Jeff for his explanation and answer. I'm accepting his answer, but I want to post an alternative implementation, which in my (admittedly limited) tests, performed slightly better. Your mileage may vary.
This removes the lock acquisition from the deleter and replaces it with an atomic flag that becomes true when the object actually gets deleted.