在使用堆栈本地对象和销毁对象之间的关系之前是否发生过什么?

问题描述

我观看了Herb Sutter的这个谈话:https://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-2-of-2

围绕1:25的标记,他谈到了为什么共享指针的原子引用计数器的减量需要获得释放存储器的顺序。

视频中的逻辑如下:

  1. 有两个线程,都同时调用fetch_sub。
  2. 线程1在减量之前使用了该对象。它将参考计数器从2减少到1。
  3. 线程2从1-> 0开始递减,并删除控制块指针。

据说获取/释放的原因是,从线程2的角度来看,线程1中对象的使用可以在线程1中的递减以下重新排序。然后,线程2从1-> 0递减。并在A行发生之前删除该对象。

以下是幻灯片

enter image description here

但是在共享指针实现中,析构函数(忽略副本分配和手动释放)在减量之前不使用该对象。这意味着在销毁共享指针之后,必须重新排序对共享指针对象的函数调用

这让我很困惑。这是我的具体问题:

  1. 真的可以在使用对象之前对析构函数进行重新排序吗?假设您有一个唯一的指针而不是一个共享的指针。如果唯一ptr的析构函数在使用前已重新排序,则您将访问已删除的内存。那不对。

  2. 从线程2的角度来看,幻灯片中线程1中的线A是否出现在线程1的减量下方,这有什么关系?假设线程1中的行A出现在减量的上方,那么一旦将缓存行推送到处理器2中,线程2的对象状态就不会被更新,并且函数调用的任何副作用对线程2来说是显而易见的吗?该代码仅在一个线程上运行。幻灯片上的相关行是:“对于线程2,线A似乎移动到线程1的减量以下”。

解决方法

赫伯·萨特(Herb Sutter)绝对知道他的东西,但是如果有人争论可能的“重新排序”,我通常会不喜欢它,因为这样的论点通常没有提供为什么实际上可能进行重新排序的全部细节。 C ++标准对指令是否或何时可以重新排序没有任何说明。它仅定义了“先发生”关系,并且可能应用的任何重新排序最终都是由于应用了先发生后关系规则(当然还有无处不在的“假设”规则)的结果。

为什么这么重要?因为事前发生关系也是定义数据竞争的基础:

如果两个表达式求值之一修改了一个内存位置(4.4),而另一个表达式读取或修改了同一个内存位置,则冲突。 [..]

如果程序的执行包含两个潜在的并发冲突操作,其中至少一个不是原子操作,并且都没有先于另一个[..]

,则执行该程序将导致数据争用。 >

众所周知,数据竞争会导致UB,因此,任何有关使用原子的多线程代码正确性的争论都必须基于事前发生。也就是说,在需要建立关系之前会发生什么,以及如何建立关系。


所以让我们看一下示例:

Thread 1:
... // use object
if (control_block_ptr->refs.fetch_sub(1,std::memory_order_release) == 1) {
  // branch not taken
}

Thread 2:
if (control_block_ptr->refs.fetch_sub(1,std::memory_order_release) == 1) {
  delete control_block_ptr;
}

您在这里的要求是delete必须在使用后发生

假设对象包含一个std::string,线程1为该字符串分配一个值,即我们有一个 write操作。当线程2删除对象时,我们还需要删除字符串成员,这显然涉及至少 read操作。这是冲突操作的教科书示例,因此我们必须确保线程1中的分配在发生之前发生,否则我们将进行数据争夺!仅使用memory_order_release,fetch_sub操作就无法与任何内容同步(释放操作本身实际上是没有意义的),因此不存在任何before-before关系。

我们如何解决?通过确保refs上的操作彼此同步。如果我们使用memory_order_acq_rel,则线程2中的fetch_sub与线程1中的fetch_sub同步,从而建立事前发生关系。 “使用对象”在线程1中的fetch_sub之前排序,在线程2中的fetch_subdelete之前排序。因此,我们有:

  • t1.use -sb-> t1.fetch_sub -sw-> t2.fetch_sub -sb-> t2.delete

其中-sb->表示“先后顺序”,-sw->表示“同步”。而且因为发生在之前是可传递的,所以这意味着t1.use发生在t2.delete之前。

另一种解决方案是在refs.load(memory_order_acquire)之前添加一个额外的delete,然后我们可以继续使用memory_order_release作为fetch_sub,因为t2.load与t1.fetch_sub同步。