【问题标题】:How `weak_ptr` and `shared_ptr` accesses are atomic`weak_ptr` 和 `shared_ptr` 访问是如何原子的
【发布时间】:2017-05-20 21:54:09
【问题描述】:
std::shared_ptr<int> int_ptr;

int main() {
    int_ptr = std::make_shared<int>(1);
    std::thread th{[&]() {
        std::weak_ptr int_ptr_weak = int_ptr;
        auto int_ptr_local = int_ptr_weak.lock();
        if (int_ptr_local) {
            cout << "Value in the shared_ptr is " << *int_ptr_local << endl;
        }
    });

    int_ptr.reset(nullptr);
    th.join();
    return 0;
}

上面的代码是线程安全的吗?我读了这个答案About thread-safety of weak_ptr,但只是想确保上面的代码是线程安全的。

我问这个的原因是,如果上面的代码确实是线程安全的,我无法理解std::weak_ptrstd::shared_ptr 接口如何使以下操作原子expired() ? shared_ptr&lt;T&gt;() : shared_ptr&lt;T&gt;(*this)。在我看来,如果不使用某种互斥锁或自旋锁,就无法使像上面这样的两行逻辑代码同步。

我了解原子增量如何与不同的共享指针实例一起工作,并且我了解shared_ptrs 本身不是线程安全的,但如果上面确实是线程安全的,那么它非常类似于线程安全的shared_ptr,我不明白上面条件中的两行代码如何在没有锁的情况下成为原子代码。

【问题讨论】:

  • "它非常像线程安全的shared_ptr" - 使用shared-shared_ptr来构造weak_ptr真的是线程安全的吗?跨度>
  • 没有什么可以阻止实现使用互斥体使lock 原子化,除非它效率不高。

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


【解决方案1】:

上面的代码线程安全吗?

我认为不是,因为 int_ptr.reset(nullptr); 正在与 std::weak_ptr int_ptr_weak = int_ptr; 竞争

我无法理解 std::weak_ptr 和 std::shared_ptr 接口如何使以下操作成为原子expired() ? shared_ptr&lt;T&gt;() : shared_ptr&lt;T&gt;(*this)

这样的操作不是原子的,因为expired() 可能会返回 false,但是当您对该值采取行动时,它可能不再准确。另一方面,如果它返回 true,则保证保持准确,只要从那时起没有人修改 this specific 的 shared_ptr 实例。也就是说,对给定 shared_ptr 的其他副本的操作不会导致它过期。

weak_ptr::lock() 实现不会使用expired()。它可能会执行类似原子比较交换的操作,其中仅当当前强引用的数量大于零时才会添加额外的强引用。

【讨论】:

  • 我问是因为 cppreference 说那行代码是原子的。 en.cppreference.com/w/cpp/memory/weak_ptr/lock我解释错了吗?
  • cppreference.com 上的写法如下: 有效返回 expired() ? shared_ptr() : shared_ptr(*this),原子执行。这应该被解释为“做一些事情来达到这个效果,但是原子性的”。
  • 您对weak_ptr::lock() 的使用确实是线程安全的,但是在从强指针构造弱指针和重置强指针之间存在不相关的竞争条件。
  • @Curious:是的,这就是为什么最后两个词是“原子执行”的原因。代码 sn-p 只描述了结果,而不是到达那里的实际过程。
  • @Curious:weak_ptr::lock() 操作对于与weak_ptr 共享状态的shared_ptrweak_ptr 的其他实例上的并发操作是安全的,您正在调用lock() on .此类操作还包括销毁另一个实例。
【解决方案2】:

这个问题有两个部分:

线程安全

代码是不是线程安全的,但这与lock()无关:
比赛存在于int_ptr.reset();std::weak_ptr int_ptr_weak = int_ptr; 之间。因为一个线程正在修改非原子变量int_ptr,而另一个线程正在读取它,根据定义,这是一场数据竞争。

这样就可以了:

int main() {
    auto int_ptr = std::make_shared<int>(1);
    std::weak_ptr<int> int_ptr_weak = int_ptr;  //create the weak pointer in the original thread
    std::thread th( [&]() {
        auto int_ptr_local = int_ptr_weak.lock();
        if (int_ptr_local) {
            std::cout << "Value in the shared_ptr is " << *int_ptr_local << std::endl;
        }
    });

    int_ptr.reset();
    th.join();
}

示例代码的原子版本expired() ? shared_ptr&lt;T&gt;() : shared_ptr&lt;T&gt;(*this)

当然,整个过程不可能是原子的。真正重要的部分是强引用计数仅在它已经大于零并且检查和增加以原子方式发生时才增加。我不知道是否有任何系统/架构特定的原语可用于此,但在 c++11 中实现它的一种方法是:

std::shared_ptr<T> lock() {
    if (!isInitialized) {
        return std::shared_ptr<T>();
    }
    std::atomic<int>& strong_ref_cnt = get_strong_ref_cnt_var_from_control_block();
    int old_cnt = strong_ref_cnt.load();
    while (old_cnt && !strong_ref_cnt.compare_exchange_weak(old_cnt, old_cnt + 1)) {
        ;
    }
    if (old_cnt > 0) {
        // create shared_ptr without touching the control block any further
    } else {
        // create empty shared_ptr
    }
}

【讨论】:

  • 感谢您的回答!我在这里感到困惑的唯一部分是执行lock() 的方法涉及一个锁(在您的示例中为自旋锁)我假设标准说lock() 函数将在无锁方式。这不是真的吗?
  • @Curious:你从哪里得到操作必须是无锁的印象?该标准只说它必须以原子方式发生,这与无锁不同。实际上,std::atomic_flag 上的操作是标准要求无锁的唯一操作。我也不确定,如果我称之为自旋锁。因为代码没有在这里获取、释放或等待任何东西
  • @Curious "在你的例子中是自旋锁" 没有自旋锁。
【解决方案3】:

不,您的代码不是线程安全的。主线程中的int_ptr.reset() 操作(这是一个写操作)和th 中的int_ptr 初始化int_weak_ptr 之间存在数据竞争(这是一个读操作)。

【讨论】:

【解决方案4】:

" std::weak_ptrstd::shared_ptr 接口如何使以下操作原子化 expired() ? shared_ptr&lt;T&gt;() : shared_ptr&lt;T&gt;(*this)"

接口没有。它在实现的内部。具体实现方式因实现而异。

【讨论】:

  • 我只是不明白那行代码如何在没有锁的情况下是原子的。你能举一个这样的实现的例子吗?
  • @Curious:那行代码不是原子的。实现将有其他代码。由于它是一个模板,您可以在您的实现中找到一个示例。
猜你喜欢
  • 2019-11-25
  • 2011-06-26
  • 2017-05-06
  • 1970-01-01
  • 1970-01-01
  • 2021-06-20
  • 2016-07-31
  • 2018-06-09
  • 2013-02-06
相关资源
最近更新 更多