Vit*_*meo 2 c++ multithreading atomic c++11 stdatomic
我有一个全局引用计数对象obj,我想通过使用原子操作来防止数据争用:
T* obj; // initially nullptr
std::atomic<int> count; // initially zero
Run Code Online (Sandbox Code Playgroud)
std::memory_order_release我的理解是,我需要在写入后使用obj,以便其他线程知道它被创建:
void increment()
{
if (count.load(std::memory_order_relaxed) == 0)
obj = std::make_unique<T>();
count.fetch_add(1, std::memory_order_release);
}
Run Code Online (Sandbox Code Playgroud)
同样,我需要std::memory_order_acquire在读取计数器时使用,以确保线程具有obj被更改的可见性:
void decrement()
{
count.fetch_sub(1, std::memory_order_relaxed);
if (count.load(std::memory_order_acquire) == 0)
obj.reset();
}
Run Code Online (Sandbox Code Playgroud)
我不相信上面的代码是正确的,但我也不完全确定为什么。我觉得obj.reset()调用之后,应该有一个std::memory_order_release操作来通知其他线程。那是对的吗?
是否还有其他可能出错的事情,或者在这种情况下我对原子操作的理解完全错误?
无论内存顺序如何,它都是错误的。
正如@MaartenBamelis 指出的那样,increment对象的并发调用被构造了两次。对于并发也是如此decrement:对象被重置两次(这可能导致双重析构函数调用)。
T* obj;请注意,声明和使用它之间存在分歧unique_ptr,但原始指针和唯一指针对于并发修改都是安全的。在实践中,resetordelete会检查指针是否为空,然后删除并将其设置为空,并且这些步骤不是原子的。
fetch_add并且fetch_sub是 fetch 和 op 而不仅仅是 op 是有原因的:如果您不使用操作期间观察到的值,则可能会出现竞争。
这段代码本质上是有争议的。如果两个线程在is 初始increment时同时调用,两个线程都会看到as ,并且都会创建(并竞相查看保留哪个副本;给定没有特殊的线程保护,如果其中两个同时设置它,可能会发生可怕的事情)。count0count0objunique_ptr
如果两个线程decrement同时(持有最后两个引用),并在fetch_sub任一调用之前完成load,则两者都会reset obj(也不好)。
如果 adecrement完成fetch_sub(to 0),则另一个线程increments 在发生之前decrement load,increment将看到countas0并重新初始化。对象是在被替换后被清除,还是被清除后被替换,或者两者的某种可怕的混合,将取决于increments是在sfetch_add之前还是之后运行。decrementload
简而言之:如果您发现自己对同一个变量使用两个单独的原子操作,并测试其中一个操作的结果(没有循环,如比较和交换循环中),那么您就错了。
更正确的代码如下所示:
void increment() // Still not safe
{
// acquire is good for the != 0 case, for a later read of obj
// or would be if the other writer did a release *after* constructing an obj
if (count.fetch_add(1, std::memory_order_acquire) == 0)
obj = std::make_unique<T>();
}
void decrement()
{
if (count.fetch_sub(1, std::memory_order_acquire) == 1)
obj.reset();
}
Run Code Online (Sandbox Code Playgroud)
但即便如此,它也不可靠;不能保证,当countis 时0,两个线程不能同时调用,并且虽然保证其中一个线程能够看到 as ,但看到它的increment线程可能会延迟,而看到它的线程则可能会延迟该对象在初始化之前就存在并使用它。fetch_addcount001
我不会发誓这里没有无互斥的解决方案,但处理与原子相关的问题几乎肯定不值得头痛。
或许可以将互斥锁限制在分支内部if(),但是获取互斥锁也是一个原子 RMW 操作(对于一个好的轻量级实现而言,这并不多),因此这不一定有很大帮助。如果您需要真正良好的读取端扩展,您需要研究RCU之类的东西而不是引用计数,以允许读者真正只读,而不是与其他读者竞争。
| 归档时间: |
|
| 查看次数: |
218 次 |
| 最近记录: |