11

我刚刚看过 Herb Sutter 的演讲:C++ 和超越 2012:Herb Sutter - atomic<> Weapons, 2 of 2

他在 std::shared_ptr 析构函数的实现中显示了错误:

if( control_block_ptr->refs.fetch_sub(1, memory_order_relaxed ) == 0 )
    delete control_block_ptr; // B

他说,由于 memory_order_relaxed,delete 可以放在 fetch_sub 之前。

在 1:25:18 - Release 没有将 B 行保留在它应该在的位置

这怎么可能?存在先发生/先排序关系,因为它们都在单线程中。我可能错了,但是 fetch_sub 和 delete 之间也存在依赖关系。

如果他是对的,哪些 ISO 项目支持这一点?

4

4 回答 4

2

想象一个释放共享指针的代码:

auto tmp = &(the_ptr->a);
*tmp = 10;
the_ptr.dec_ref();

如果 dec_ref() 没有“释放”语义,编译器(或 CPU)可以将 dec_ref() 之前的内容移动到它之后(例如):

auto tmp = &(the_ptr->a);
the_ptr.dec_ref();
*tmp = 10;

这是不安全的,因为 dec_ref() 也可以同时从其他线程调用并删除对象。因此,在 dec_ref() 之前,它必须有一个“释放”语义才能留在那里。

现在让我们想象对象的析构函数如下所示:

~object() {
    auto xxx = a;
    printf("%i\n", xxx);
}

此外,我们将对示例进行一些修改,并将有 2 个线程:

// thread 1
auto tmp = &(the_ptr->a);
*tmp = 10;
the_ptr.dec_ref();

// thread 2
the_ptr.dec_ref();

然后,“聚合”代码将如下所示:

// thread 1
auto tmp = &(the_ptr->a);
*tmp = 10;
{ // the_ptr.dec_ref();
    if (0 == atomic_sub(...)) {
        { //~object()
            auto xxx = a;
            printf("%i\n", xxx);
        }
    }
}

// thread 2
{ // the_ptr.dec_ref();
    if (0 == atomic_sub(...)) {
        { //~object()
            auto xxx = a;
            printf("%i\n", xxx);
        }
    }
}

但是,如果我们只有 atomic_sub() 的“释放”语义,则可以这样优化这段代码:

// thread 2
auto xxx = the_ptr->a; // "auto xxx = a;" from destructor moved here
{ // the_ptr.dec_ref();
    if (0 == atomic_sub(...)) {
        { //~object()
            printf("%i\n", xxx);
        }
    }
}

但是那样,析构函数不会总是打印“a”的最后一个值(这段代码不再是无竞争的)。这就是为什么我们还需要为 atomic_sub 获取语义(或者,严格来说,当计数器在递减后变为 0 时,我们需要一个获取屏障)。

于 2015-02-10T01:07:03.680 回答
1

看起来他正在谈论共享对象本身上的操作同步,这在他的代码块上没有显示(结果 - 令人困惑)。

这就是他提出的原因acq_rel——因为对对象的所有操作都应该在其销毁之前发生,一切都井然有序。

但我仍然不确定他为什么谈论deletefetch_sub.

于 2013-02-14T19:23:10.563 回答
1

这是一个迟到的回复。

让我们从这个简单的类型开始:

struct foo
{
    ~foo() { std::cout << value; }
    int value;
};

我们将在 a 中使用这种类型shared_ptr,如下所示:

void runs_in_separate_thread(std::shared_ptr<foo> my_ptr)
{
    my_ptr->value = 5;
    my_ptr.reset();
}

int main()
{
    std::shared_ptr<foo> my_ptr(new foo);
    std::async(std::launch::async, runs_in_separate_thread, my_ptr);
    my_ptr.reset();
}

两个线程将并行运行,共享一个foo对象的所有权。

通过正确的shared_ptr实现(即带有 的实现memory_order_acq_rel),该程序具有已定义的行为。该程序将打印的唯一值是5.

如果实现不正确(使用memory_order_relaxed),则没有这样的保证。行为未定义,因为 foo::value引入了数据竞争。只有在主线程中调用析构函数时才会出现问题。使用宽松的内存顺序,其他线程中的写入foo::value可能不会传播到主线程中的析构函数。5可以打印的值不是。

那么什么是数据竞赛?好吧,查看定义并注意最后一个要点:

当一个表达式的计算写入一个内存位置而另一个计算读取或修改相同的内存位置时,这些表达式被称为冲突。具有两个冲突评估的程序存在数据竞争,除非其中任何一个

  • 两个冲突的评估都是原子操作(见 std::atomic)
  • 其中一个冲突的评估发生在另一个之前(参见 std::memory_order)

在我们的程序中,一个线程将写入foo::value,​​一个线程将读取foo::value。这些应该是顺序的;写入foo::value应始终发生在读取之前。直观地说,它们是有意义的,因为析构函数应该是对象上发生的最后一件事。

memory_order_relaxed但不提供此类订购保证,因此 memory_order_acq_rel是必需的。

于 2016-07-07T06:57:45.660 回答
0

在谈话中 Herb 表示memory_order_release不是memory_order_relaxed,但放松会带来更多问题。

除非delete control_block_ptr访问control_block_ptr->refs(它可能没有),否则原子操作不会携带依赖关系到删除。删除操作可能不会触及控制块中的任何内存,它可能只是将该指针返回给 freestore 分配器。

但我不确定 Herb 是在谈论编译器在原子操作之前移动删除,还是只是指副作用何时对其他线程可见。

于 2013-02-14T18:53:16.453 回答