【问题标题】:Why is std::weak_ptr::expired optimized away?为什么 std::weak_ptr::expired 被优化掉了?
【发布时间】:2015-03-22 14:18:24
【问题描述】:

在下面的代码中,while ( !Ref.expired() ); 被愉快地优化成一个无限循环。如果代码行改为while ( !Ref.lock() );。一切都按预期工作。所以真的有两个问题:

1) 当std::weak_ptr::expired() 访问内存围栏计数器时,编译器如何优化过期?

2) Ref.lock() 真的安全吗,或者这也可以优化掉?

下面的示例代码。

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>

class A
{
public:

    A() 
    { 
        m_SomePtr = std::make_shared<bool>( false );
    }

    virtual ~A()
    {
        std::weak_ptr<bool> Ref = m_SomePtr;
        m_SomePtr.reset();

        // Spin (will be optimised into an infinite loop in release builds)
        while ( !Ref.expired() );
    }

    std::shared_ptr<bool> GetPtr() const { return m_SomePtr; }

private:
    std::shared_ptr<bool> m_SomePtr;
};

class B
{
public:
    B( std::shared_ptr<bool> SomePtr ) : m_Ref( SomePtr ) {}

    void LockPtr() { m_SomePtr = m_Ref.lock(); }
    void UnLockPtr() { m_SomePtr.reset(); }

private:
    std::shared_ptr<bool> m_SomePtr;
    std::weak_ptr<bool> m_Ref;
};

int main()
{
    std::unique_ptr<A> a( new A() );
    std::unique_ptr<B> b( new B( a->GetPtr() ) );

    b->LockPtr();

    std::cout << "Starting " << std::endl;

    std::thread first( [&]()
    {
        std::this_thread::sleep_for( std::chrono::seconds( 5 ) );
        b->UnLockPtr();
    } );

    std::thread second( [&]()
    {
        a.reset( nullptr );
    } );

    first.join();
    second.join();

    std::cout << "Complete" << std::endl;
    return 0;
}

【问题讨论】:

  • joyfully 优化点赞。你是在建议机器和/或程序可以玩得开心吗?
  • @Walter 我有一个明显的印象,即编译器对自己非常满意。然而,我非常不高兴。编译器和我关系不和。
  • @PuffOfHotAir:那个编译器和大家的关系很混乱。
  • 是什么让您产生了柜台被围起来的想法?我在标准中找不到任何说明它的内容,并且 gcc 的 libstdc++ 在这里与 msvc 一致,因为它对计数器使用宽松的内存顺序(即使 gcc 默认不再进行循环优化;尝试使用-faggressive-loop-optimizations)。另外,按照我的阅读方式,在 weak_ptr 上调用 lock() 会引入数据竞争。
  • @Wintermute 我没有标准,但link 似乎讨论了关于lock() 的歧义,可能导致executed atomically。关键是从线程安全的角度来看,代码正确expired() 必须访问线程安全计数器),但优化器删除了留下非功能代码的调用。跨度>

标签: c++ multithreading c++11 visual-c++ shared-ptr


【解决方案1】:

您的程序不正确;共享所有权指针工具不打算用于同步。

[intro.multithread]/24:

实现可能假设任何线程最终都会执行以下操作之一:
— 终止,
— 调用库 I/O 函数,
— 访问或修改 volatile 对象,或
— 执行同步操作或原子操作。

std::weak_ptr::expired() 不是同步操作或原子操作;该标准所说的只是它没有引入数据竞争。由于对Library defect 2316std::weak_ptr::lock() 的解析被认为是原子操作,因此回答2)您使用Ref.lock() 的代码自C++14 起有效。

现在,确实,如果您要尝试使用语言和库工具创建自己的库实现 weak_ptr,它必然会使用同步和/或原子操作工具,因此用户提供的 @987654326 @ 可以继续运行(根据 [intro.multithread]/2 和 /25,实现必须确保线程最终取得进展)。但实现没有义务将其自己的库限制为语言和库设施。

我不完全确定编译器如何优化对expired() 的访问。我猜 MSVC 库正在利用 x86 内存模型的某些方面,编译器/优化器观察到的 C++ 内存模型无法保证。

【讨论】:

  • Nor is std::weak_ptr::lock() a synchronization operation or an atomic operation:这不与 [util.smartptr.weak.obs]/5 冲突吗,它(至少在 N4296 中)明确表示 , executed atomically
  • @ecatmur 是否有任何编译器真正使lock()非原子该问题被添加到标准中?那将是非常不直观的......
  • 挖掘标题显示 MSVC 的 stdlib 使用引用计数的非原子读取来在 x86 上实现 weak_ptr::expiredshared_ptr::use_count。我们可以整天争论它是否严格符合要求,但我认为很明显它的 QoI 很差。
  • @Puff Of Hot Air 从多个线程读取是“安全的”,因为它不会引入数据竞争;但除非标准这么说,否则它不是一个同步点,这意味着它不能保证取得进展。 (并且不能保证取得进展的线程是无效的。)标准本可以选择制作use_countexpired 同步点,如果不这样做可能被视为缺陷,但这样做将强加于实现并可能会促进不明确的代码,即使用共享所有权指针工具进行同步。
  • @curiousguy 嗯,也许我可以更清楚一点。下一段指出:25 - 实现应确保原子或同步操作分配的最后一个值(按修改顺序)将在有限的时间内对所有其他线程可见。 @987654335 @ 不是原子或同步操作分配的值,并且该循环内没有其他原子读取或同步操作,因此与标准一致,不应观察到更改。
猜你喜欢
  • 2023-03-15
  • 2015-03-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-12
  • 2017-06-03
  • 1970-01-01
  • 2017-06-10
相关资源
最近更新 更多