`weak_ptr`和`shared_ptr`的访问是原子性的吗?

7
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;
}

上面的代码是线程安全的吗?我看了这篇回答 关于 weak_ptr 的线程安全性,但我只想确保上面的代码是线程安全的。
我问这个问题的原因是,如果上述代码确实是线程安全的,我就无法理解为什么 std::weak_ptr 和 std::shared_ptr 接口会使以下操作具有原子性 expired() ? shared_ptr<T>() : shared_ptr<T>(*this)。在我看来,像上面那样进行两行逻辑代码同步执行的操作似乎是不可能的,除非使用某种互斥体或自旋锁。
我理解如何通过不同 shared_ptr 实例的原子增量工作,我也理解 shared_ptr 本身不是线程安全的,但如果上面的内容确实是线程安全的,它就非常像一种线程安全的 shared_ptr,我不明白像上面的条件语句中的两行代码如何在没有锁的情况下做到原子化。

“它非常类似于线程安全的shared_ptr” - 使用共享的shared_ptr来构造weak_ptr真的是线程安全的吗? - Holt
除了效率不高之外,实现可以使用互斥锁使“lock”操作具有原子性。 - Oktalist
4个回答

5
上面的代码是否线程安全?我认为不是,因为 int_ptr.reset(nullptr);std::weak_ptr int_ptr_weak = int_ptr; 存在竞争关系。
我无法理解 std::weak_ptr 和 std::shared_ptr 接口如何使以下操作原子化 expired() ? shared_ptr<T>() : shared_ptr<T>(*this)。这种操作并不是原子化的,因为 expired() 可能返回 false,但是在您采取行动时,它可能不再准确。另一方面,如果它返回 true,则保证始终保持准确性,只要没有人修改了此特定 shared_ptr 实例。也就是说,对于给定 shared_ptr 的其他副本的操作不能导致其过期。 weak_ptr::lock() 的实现不会使用 expired()。它可能会执行类似于原子比较交换的操作,仅当当前强引用数大于零时才添加额外的强引用。

我问这个问题是因为cppreference说那行代码是原子的。http://en.cppreference.com/w/cpp/memory/weak_ptr/lock 我理解错了吗? - Curious
1
cppreference.com 上的措辞如下:“_Effectively returns expired() ? shared_ptr<T>() : shared_ptr<T>(*this), executed atomically_”。这应该被解释为“以类似的方式执行某些操作,但是原子地执行”。 - Joseph Artsimovich
3
您对 weak_ptr::lock() 的使用确实是线程安全的,但是在从强指针构造弱指针并重置该强指针之间存在一个无关的竞态条件。 - Joseph Artsimovich
1
@Curious:是的,这就是为什么最后两个词是“原子地执行”。代码片段只描述了结果,而不是到达那里的实际过程。 - MSalters
2
@Curious:weak_ptr::lock() 操作在与共享状态的其他 shared_ptrweak_ptr 实例的并发操作方面是安全的,这些实例与您调用 lock()weak_ptr 共享状态。这样的操作还包括另一个实例的销毁。 - Joseph Artsimovich
显示剩余7条评论

4

这个问题有两个部分:

线程安全

代码不是线程安全的,但这与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<T>() : shared_ptr<T>(*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
@Curious:你从哪里得到这个操作必须是无锁的印象?标准只要求它必须原子化,这与无锁不同。实际上,std::atomic_flag 上的操作是标准要求为无锁的唯一操作。此外,我不确定是否应该将其称为自旋锁,因为代码在这里没有获取、释放或等待任何东西。 - MikeMB
@Curious "在你的例子中有一个自旋锁",但实际上并没有自旋锁。 - curiousguy

3
不,你的代码不是线程安全的。主线程中的int_ptr.reset()操作(写操作)和从int_ptr初始化int_weak_ptrth中的读操作之间存在数据竞争。

但是cppreference似乎说lock方法会原子地执行其逻辑。http://en.cppreference.com/w/cpp/memory/weak_ptr/lock - Curious
2
@Curious 这个答案没有提到 lock - Oktalist

1

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

这并不是接口本身实现的,而是内部实现的。具体的实现方式会因实现而异。


我只是不明白那行代码怎么能够在没有锁的情况下是原子的。你能否给一个这样实现的例子? - Curious
1
@Curious:那行代码不是原子的。实现将有其他代码。由于它是一个模板,您只需在实现中找到一个示例即可。 - MSalters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接