当共享的托管对象永远不会同时被访问时,在生产者和使用者之间使用 std::shared_ptr 是否安全?

Is it safe to use std::shared_ptr between productor and consumer when the shared managed object would never be acessed at the same time?

提问人:John 提问时间:10/8/2023 最后编辑:John 更新时间:10/9/2023 访问量:79

问:

下面是演示代码片段。 是否可以删除它,因为它本身是线程安全的,并且生产者和消费者永远不会同时访问托管对象?mutexstd::shared_ptr

#include <memory>
#include <mutex>
#include <thread>

struct Demo {
    int i;
    float f;
};

using SCDemoPtr = std::shared_ptr<Demo>;

class ResultManager {
public:
    ResultManager();
    void SetFrontRes(SCDemoPtr oms_result);
    void SetBackRes(SCDemoPtr oms_result);
    void MergeResults();

private:
    std::mutex front_mutex;
    std::mutex back_mutex;
    SCDemoPtr front_result;
    SCDemoPtr back_result;
};

ResultManager::ResultManager() {}

void ResultManager::SetFrontRes(SCDemoPtr oms_res) {
    std::lock_guard<std::mutex> guard(front_mutex);
    front_result = oms_res;
}

void ResultManager::SetBackRes(SCDemoPtr oms_res) {
    std::lock_guard<std::mutex> guard(back_mutex);
    back_result = oms_res;
}

void ResultManager::MergeResults() {
    SCDemoPtr cur_front_res;
    SCDemoPtr cur_back_res;

    {
        std::lock_guard<std::mutex> guard(front_mutex);
        cur_front_res = front_result;
    }

    {
        std::lock_guard<std::mutex> guard(back_mutex);
        cur_back_res = back_result;
    }

    if (cur_front_res == nullptr || cur_back_res == nullptr) {
        return;
    }

    SCDemoPtr merge_res(new Demo);
    merge_res->i = cur_front_res->i + cur_back_res->i;
}

int main() {
    ResultManager res_manager;

    std::thread producer1([&res_manager]() {
        while (true) {
            SCDemoPtr res(new Demo);
            res->i = 1;
            res->f = 3.14;
            res_manager.SetFrontRes(res);
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    });

    std::thread producer2([&res_manager]() {
        while (true) {
            SCDemoPtr res(new Demo);
            res->i = 1;
            res->f = 3.14;
            res_manager.SetBackRes(res);
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    });

    while (true) {
        res_manager.MergeResults();
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    producer1.join();
    producer2.join();

    return 0;
}
C++ 线程安全 C++14 共享 PTR 智能指针

评论


答:

2赞 user17732522 10/8/2023 #1

是否可以删除互斥锁,因为 std::shared_ptr 本身是线程安全的

不,对 本身的访问不是线程安全的,并且行为与访问任何其他对象没有任何不同。当另一个线程尝试访问它时尝试修改它是一种数据争用,并导致未定义的行为。std::shared_ptr

线程安全的唯一一点是托管对象的生存期,它保证被一个线程销毁。std::shared_ptr

评论

0赞 John 10/8/2023
感谢您的回复。我对你的回答还有一个问题。我认为即使获得 1 之前的锁,也不会取消引用空的共享指针(即 nullptr),因为如果满足,该函数将立即返回。如果我错了,请告诉我。MergeResults()producerproducer2(cur_front_res == nullptr || cur_back_res == nullptr)
0赞 user17732522 10/8/2023
@John 哦,是的。对不起,我一定忽略了那句话。
1赞 Jan Schultke 10/8/2023 #2

不,这不安全。您将不同级别的线程安全混为一谈:

  1. 指向对象的线程安全。这将由 确保。(不是你的情况)std::shared_ptr<std::atomic<T>>

  2. 引用计数器的线程安全。这是由任何 .这允许您让多个线程同时使用拥有相同线程的单独对象。(不是你的情况)std::shared_ptr<T>std::shared_ptr<T>T

  3. 智能指针本身的线程安全。这是由 std::atomic<std::shared_ptr<T>> 或带有保护它的 a 来确保的。(您的情况)std::shared_ptrstd::mutex

您正在一个地方使用和写入 在另一个地方。这只是线程安全的,因为你要确保互斥。如果删除互斥锁,则会出现数据争用。back_resultcur_back_res = back_result;back_result = oms_res;

您使用的是 C++14,因此 std::atomic<std::shared_ptr> 专业化不可用,但您可以使用旧的无实用程序函数进行原子操作:

// atomically load from a std::shared_ptr
cur_back_result = std::atomic_load(&back_result);

// atomically store to a std::shared_ptr
std::atomic_store(&back_result, &oms_res);

如果使用这些函数,则不再需要互斥锁来保护 .std::shared_ptr

注意:这些函数的显式版本还允许您指定内存顺序。如果您不熟悉此主题,请参阅每个memory_order是什么意思?

Смотритетакже: std::shared_ptr 和 std::atomic<std::shared_ptr> 又名 std::experimental::atomic_shared_ptr有什么区别?

评论

0赞 John 10/8/2023
指向对象的线程安全?如何以正确的方式理解这一点?据了解,如果共享指针管理的对象由两个或多个线程读/写,则应受到保护。
0赞 Jan Schultke 10/8/2023
@John是的,应该是。 不能确保所指向的任何内容的线程安全,它只是线程安全地计算对它的引用。为了完整起见,我刚刚列出了这一点,因为在三个不同的级别上,可能需要确保线程安全。std::shared_ptr