并发读取非原子变量

时间:2018-02-18 18:02:36

标签: c++ c++11 concurrency language-lawyer atomic

我在尝试实现共享指针时遇到了这个问题。让我们关注托管数据指针。它的寿命可分为三个阶段:

  1. 没有并发访问权限的构建。
  2. 并发读取(无写入)。
  3. 在没有并发访问权限的情况下销毁。这可以通过引用计数来保证。
  4. 我的问题是,鉴于这种情况,指针是否必须是原子的?我认为这相当于:如果指针不是原子的,第2阶段会导致未定义的行为吗?理想情况下,我希望从理论(语言 - 律师)的观点和实践的角度来听取答案。例如,如果不是原子的,第2阶段理论上可能是未定义的行为,但在实际平台上实际上是可行的。为了实现共享指针,如果非原子是正常的,则托管指针可以是unique_ptr<T>,否则它必须是atomic<T*>

    更新

    我找到标准文本(第1.10节第21页):

      

    如果程序包含两个,则程序的执行包含数据竞争   不同线程中的冲突操作,其中至少有一个不是   原子,并没有发生在另一个之前。任何这样的数据竞争   导致未定义的行为。

    我猜并发读取不会被归类为冲突行为。有人可以找到一些关于此的标准文本吗?

4 个答案:

答案 0 :(得分:3)

规则是如果多个线程同时访问同一个对象并且这些线程中至少有一个正在修改数据,那么您将遇到数据争用,并且程序的行为未定义。如果没有人修改对象,那么并发访问就没有问题。

答案 1 :(得分:1)

自己找到答案。引用自C++ Concurrency in Action的第5.1.2节第一段:

  

[...]如果两个线程都没有更新内存位置,那你很好;   只读数据不需要保护或同步。如果是   线程正在修改数据,有可能进行竞赛   条件,如第3章所述。

答案 2 :(得分:1)

对任何变量的并发读取,无论是否为原子,都不构成数据竞争,因为[intro.multithread]中找到了冲突的评估:

  

两个表达式评估冲突如果其中一个修改了内存位置而另一个访问或修改了相同的内存位置。

最近,这已经转移到[intro.races],措辞非常微妙

  

两个表达式评估冲突如果其中一个修改了内存位置而另一个读取或修改了相同的内存位置。

访问读取的更改发生在草稿n4296和n4431之间。多线程部分的分裂发生在n4582和n4604之间。

答案 3 :(得分:0)

所以我想你是在谈论持有计数器的结构和指向拥有数据的指针:

template<class ValueType>
struct shared_counter{
  std::atomic<int> count=0;
  const std::unique_ptr<ValueType> ptr;
  //Your question is: Does ptr should be atomic?
  //... This is a dumb implementation, only focusing on the subject.
  };

实际上,ptr不需要是原子的,因为如果引用计数是适当的,那么在解析shared_counter之前,对ptr的所有访问都将被排序。

为了确保在shared_ptr的析构函数内部,计数器通过具有获取 - 释放内存顺序的读 - 修改 - 写入递减:

template<class ValueType>
struct shared_ptr{
   shared_counter<ValueType>* counted_ptr;
   //...
   void reset(){
     if (counted_ptr->count.fetch_sub(1,std::memory_order_acq_rel) == 1)
       counter_ptr->~shared_counter<ValueType>();
     counter_ptr=nullptr;
     }
   };

由于这个内存顺序,如果在线程A中获取的count值为1,这意味着在所有其他线程中指向同一shared_counter的其他 shared_ptrs将无法访问这个shared_counter。内存顺序确保在这些其他线程中对此shared_counter执行的访问将在线程A中获取值1之前发生。(在其他线程中释放 - &gt;在将调用析构函数的线程中获取)。

因此,没有必要让ptr成为原子,因为计数器递减会导致足够的排序。