关于shared_ptr析构函数中的实现错误的困惑

时间:2013-02-14 17:54:45

标签: c++ c++11 atomic lock-free memory-model

我刚看过Herb Sutter的演讲:C++ and Beyond 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,删除可以放在fetch_sub之前。

  

在1:25:18 - 释放不保持在B线下面,它应该是

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

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

4 个答案:

答案 0 :(得分:1)

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

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时我们需要一个获取障碍)。

答案 1 :(得分:1)

这是一个迟到的回复。

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

k

我们将在struct foo { ~foo() { std::cout << value; } int value; }; 中使用此类型,如下所示:

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以外的值。

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

  

当表达式的评估写入内存位置而另一个评估读取或修改相同的内存位置时,表达式会发生冲突。除非

,否则具有两个冲突评估的程序会进行数据竞争      
      
  • 两个冲突的评估都是原子操作(参见std :: atomic)
  •   
  • 其中一个冲突的评估发生在另一个之前(参见std :: memory_order)
  •   

在我们的程序中,一个线程将写入5,一个线程将写入 从foo::value读取。这些应该是顺序的;写 在阅读之前应始终发生foo::value。直观地说,它 理所当然,他们会像析构者一样应该是最后一个 对象发生的事情。

foo::value不提供此类排序保证,因此需要memory_order_relaxed

答案 2 :(得分:0)

在谈话中,Herb显示memory_order_release而非memory_order_relaxed,但放松会有更多问题。

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

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

答案 3 :(得分:0)

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

这就是他放acq_rel的原因 - 因为对象的所有动作都应该在它被破坏之前发生,一切都按顺序进行。

但我仍然不确定他为何会谈到将deletefetch_sub进行交换。