在多线程服务器中,一个线程(编写器)定期更新数据库中的数据,其他线程(读取器)使用此数据处理用户的请求。
我尝试使用读/写锁来满足此请求,但是性能太差了,因此需要寻找其他东西。
我从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的线程安全级别。这种方法有问题吗?或其他建议,以满足我的要求
答案 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_ptr
和atomic_weak_ptr
来代替这些功能的使用。从C ++ 20开始:不推荐使用这些功能,而推荐使用
std::atomic
模板的专业化:std::atomic<std::shared_ptr>
和std::atomic<std::weak_ptr>
。
此外,您应该让Data
析构函数进行所有清理,以便不必等到读取器线程释放_main_data
来手动清理它。
或者,您可以使用std::atomic
和boost::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只是指针的容器。它不是线程安全的。您可以做到,但是使用锁更容易。