我正在尝试在std :: unordered_map之上实现一个线程安全的包装器类 如下所示开始和结束功能是否安全?
std::unordered_map<Key, T, Hash, Pred, Alloc> umap;
iterator begin() {
return umap.begin();
}
iterator end() {
return umap.end();
}
如果复制/移动操作符=实现
中有任何明显错误,请注释 concurrent_unordered_map& operator=(const concurrent_unordered_map& other) ;
{
if (this!=&other) {
std::lock(entry_mutex, other.entry_mutex);
std::lock_guard<boost::shared_mutex> _mylock(entry_mutex, std::adopt_lock);
std::shared_lock<boost::shared_mutex> _otherlock(other.entry_mutex, std::adopt_lock);
umap = other.umap;
}
return *this;
}
concurrent_unordered_map& operator=(concurrent_unordered_map&& other)
{
if (this!=&other) {
std::lock(entry_mutex, other.entry_mutex);
std::lock_guard<boost::shared_mutex> _mylock(entry_mutex, std::adopt_lock);
std::shared_lock<boost::shared_mutex> _otherlock(other.entry_mutex, std::adopt_lock);
umap = std::move(other.umap)
}
return *this;
}
由于 MJV
答案 0 :(得分:4)
即使您同步每个方法调用,也无法创建提供与基础标准容器相同接口的线程安全容器。这是因为接口规范本身并不打算在多线程环境中使用。
以下是一个示例:假设您有多个并发插入同一容器对象的线程:
c->insert(new_value);
因为您同步了每个方法调用,所以这个工作正常,这里没问题。
但与此同时,另一个线程试图遍历容器中的所有元素:
auto itr = c->begin();
while (itr != c->end())
{
// do something with itr
++itr;
}
我用这种方式编写它来解决问题:即使对开始和结束的调用是内部同步的,你也不能以原子方式执行“循环遍历所有元素”操作,因为你需要多个方法调用来完成这个任务。一旦其他线程在循环运行时向容器插入内容,此方案就会中断。
因此,如果您想拥有一个可以在没有外部同步的情况下使用的容器,那么您需要一个线程安全的接口。例如,“循环遍历所有元素”任务可以通过提供for_each方法原子地完成:
c.for_each([](const value_type& value)
{
// do something with value
});
答案 1 :(得分:2)
您不能简单地同步每个方法并获取线程安全对象,因为某些操作需要多个方法调用,如果容器在方法调用之间发生变异,则会中断。
一个典型的例子就是迭代。
线程安全的简单方法是滥用C ++ 14这样的功能:
template<class T>
struct synchronized {
// one could argue that rvalue ref qualified version should not be
// synchronized... but I think that is wrong
template<class F>
std::result_of_t< F(T const&) > read( F&& f ) const {
auto&& lock = read_lock();
return std::forward<F>(f)(t);
}
template<class F>
std::result_of_t< F(T&) > write( F&& f ) {
auto&& lock = write_lock();
return std::forward<F>(f)(t);
}
// common operations, useful rvalue/lvalue overloads:
// get a copy of the internal guts:
T copy() const& { return read([&](auto&&t)->T{return t;}); }
T copy() && { return move(); }
T move() { return std::move(*this).write([&](auto&&t)->T{return std::move(t);}); }
private:
mutable std::shared_timed_mutex mutex;
std::shared_lock<std::shared_timed_mutex> read_lock() const {
return std::shared_lock<std::shared_timed_mutex>(mutex);
}
std::unique_lock<std::shared_timed_mutex> write_lock() {
return std::unique_lock<std::shared_timed_mutex>(mutex);
}
T t;
public:
// relatively uninteresting boilerplate
// ctor:
template<class...Args>
explicit synchronized( Args&&... args ):
t(std::forward<Args>(args)...)
{}
// copy ctors: (forwarding constructor above means need all 4 overloads)
synchronized( synchronized const& o ) :t(std::forward<decltype(o)>(o).copy()) {}
synchronized( synchronized const&& o ):t(std::forward<decltype(o)>(o).copy()) {}
synchronized( synchronized & o ) :t(std::forward<decltype(o)>(o).copy()) {}
synchronized( synchronized && o ) :t(std::forward<decltype(o)>(o).copy()) {}
// copy-from-T ctors: (forwarding constructor above means need all 4 overloads)
synchronized( T const& o ) :t(std::forward<decltype(o)>(o)) {}
synchronized( T const&& o ):t(std::forward<decltype(o)>(o)) {}
synchronized( T & o ) :t(std::forward<decltype(o)>(o)) {}
synchronized( T && o ) :t(std::forward<decltype(o)>(o)) {}
};
看起来很模糊,但效果很好:
int main() {
synchronized< std::unordered_map<int, int> > m;
m.write( [&](auto&&m) {
m[1] = 2;
m[42] = 13;
});
m.read( [&](auto&&m) {
for( auto&& x:m ) {
std::cout << x.first << "->" << x.second << "\n";
}
});
bool empty = m.read( [&](auto&&m) {
return m.empty();
});
std::cout << empty << "\n";
auto copy = m.copy();
std::cout << copy.empty() << "\n";
synchronized< std::unordered_map<int, int> > m2 = m;
m2.read( [&](auto&&m) {
for( auto&& x:m ) {
std::cout << x.first << "->" << x.second << "\n";
}
});
}
我们的想法是将您的操作粘贴到lambda中,该lambdas在同步的上下文中执行。
编码风格有点模糊,但并非难以管理(至少使用C ++ 14功能)。
C ++ 11的一个很好的特性是,即使来自两个不同的线程,同一容器上的两个const
操作也是合法的。因此,read
只是简单地将const
引用传递给容器,而且几乎所有可以在其中执行的操作都是合法的,可以与另一个线程并行执行。
答案 2 :(得分:0)
有一个线程安全的std::unordered_map
实现是可能的(但通常不是很有用) - 问题是每个迭代器对象都需要锁定一个递归互斥锁,直到它的析构函数运行。这不仅会有点慢,而且迭代器会因内存使用而膨胀,还存在功能性问题:即使当它们不是“当前”时,保持迭代器也并不罕见。用于读取或写入容器(例如,对于某些二级索引,或作为&#34;游标&#34;,或者因为在使用它们之后,它们的销毁是懒惰的,直到封闭的范围退出或拥有对象被销毁):这意味着其他线程可能会被阻塞很长一段时间,实际上,容器操作周围的程序逻辑可能构成一种死锁。