一个编写器中的std :: shared_ptr许多读取器设计是线程安全的吗?

时间:2019-04-15 16:45:33

标签: c++ multithreading shared-ptr

在多线程服务器中,一个线程(编写器)定期更新数据库中的数据,其他线程(读取器)使用此数据处理用户的请求。

我尝试使用读/写锁来满足此请求,但是性能太差了,因此需要寻找其他东西。

我从https://en.cppreference.com/w/cpp/memory/shared_ptr阅读,说:

所有成员函数(包括副本构造函数和副本分配)可以由shared_ptr的不同实例上的多个线程调用,而无需额外的同步,即使这些实例是副本并共享同一对象的所有权。 / p>

然后经过一番研究,我使用std :: shared_ptr来做到这一点。代码类似于下面的内容。

// this class is singleton
class DataManager{
public:
    // all the reader thread use this method to get data and release shared_ptr 
    // at the end of user's request
    std::shared_ptr<Data> get_main_ptr(){
        return _main_data;
    }
private:
    // data1
    std::shared_ptr<Data> _main_data;
    // data2
    std::shared_ptr<Data> _back_data;

    // read database, write data in to _data
    void update_data(std::shared_ptr<Data> _data);

    // this function called at a separate thread every 10 min
    bool reload_data(){
        // write data in back pointer
        update_data(_back_data);

        //save the _main_data
        std::shared_ptr<Data> old_ptr = _main_data;

        //exchange pointer, reader thread hold the copy of _main_data
        _main_data = _back_data;

        // wait until reader threads release all copy of _main_data
        while(old_ptr.use_count() != 1) {
            sleep(5);
        }

        // clear the data
        old_ptr->clear();
        _back_data = old_ptr;
        return;
}

}

此方法似乎在生产环境中有效。但是我不太确定,也不了解shared_ptr的线程安全级别。这种方法有问题吗?或其他建议,以满足我的要求

2 个答案:

答案 0 :(得分:3)

似乎您重新分配了线程之间共享的shared_ptr

_main_data = _back_data;

如果另一个线程同时读取或复制_main_data,则可能会得到损坏的副本。

分配给shared_ptr并不是线程安全的,因为shared_ptr包含两个指针成员,并且不能同时原子地更新它们。参见shared_ptr

  

如果多个执行线程在不同步的情况下访问同一个shared_ptr,并且这些访问中的任何一个都使用了shared_ptr的非常量成员函数,则会发生数据争用;

要解决该竞争条件,代码需要使用atomic_store

atomic_store(&_main_data, _back_data);

读者必须这样做:

auto main_data = atomic_load(&_main_data);

Notes section很有帮助:

  

这些功能通常使用互斥锁实现,互斥锁存储在全局哈希表中,在该哈希表中,指针值用作键。

     

为避免数据争用,一旦将共享指针传递给这些函数中的任何一个,就不能以非原子方式对其进行访问。特别是,您必须先原子地将其shared_ptr原子加载到另一个shared_ptr对象,然后再通过第二个对象进行解引用,才能取消引用。{p>      

并发TS提供了原子智能指针类atomic_shared_ptratomic_weak_ptr来代替这些功能的使用。

     

从C ++ 20开始:不推荐使用这些功能,而推荐使用std::atomic模板的专业化:std::atomic<std::shared_ptr>std::atomic<std::weak_ptr>


此外,您应该让Data析构函数进行所有清理,以便不必等到读取器线程释放_main_data来手动清理它。


或者,您可以使用std::atomicboost::intrusive_ptr使数据指针的更新是线程安全的,原子的,无等待的和无泄漏的。

使用boost::intrusive_ptr而不是std::shared_ptr的好处是,前者可以从普通指针中线程安全地创建,因为原子引用计数存储在对象内部。

工作示例:

#include <iostream>
#include <atomic>

#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <boost/smart_ptr/intrusive_ref_counter.hpp>

struct Data
    : boost::intrusive_ref_counter<Data, boost::thread_safe_counter>
{};

using DataPtr = boost::intrusive_ptr<Data>;

class DataAccessor
{
    std::atomic<Data*> data_ = 0;

public:
    ~DataAccessor() {
        DataPtr{data_.load(std::memory_order_acquire), false}; // Destroy data_.
    }

    DataPtr get_data() const {
        return DataPtr{data_.load(std::memory_order_acquire)};
    };

    void set_data(DataPtr new_data) {
        DataPtr old_data{data_.load(std::memory_order_relaxed), false}; // Destroy data_.
        data_.store(new_data.detach(), std::memory_order_release);
    }
};

int main() {
    DataAccessor da;

    DataPtr new_data{new Data};
    da.set_data(new_data);
    DataPtr old_data = da.get_data();
    std::cout << (new_data == old_data) << '\n';
}

valgrind运行:

$ valgrind ./test
==21502== Memcheck, a memory error detector
==21502== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==21502== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==21502== Command: ./test
==21502== 
1
==21502== 
==21502== HEAP SUMMARY:
==21502==     in use at exit: 0 bytes in 0 blocks
==21502==   total heap usage: 4 allocs, 4 frees, 73,736 bytes allocated
==21502== 
==21502== All heap blocks were freed -- no leaks are possible
==21502== 
==21502== For counts of detected and suppressed errors, rerun with: -v
==21502== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

答案 1 :(得分:1)

您跳过了这一部分:

  

如果多个执行线程访问同一个shared_ptr而没有   同步,并且其中任何访问都使用非常量成员   shared_ptr的功能,那么就会发生数据争用

shared_ptr只是指针的容器。它不是线程安全的。您可以做到,但是使用锁更容易。

相关问题