std :: unordered_map上的threadsafe包装器

时间:2015-01-06 10:16:57

标签: c++ multithreading c++11 unordered-map

我正在尝试在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

3 个答案:

答案 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引用传递给容器,而且几乎所有可以在其中执行的操作都是合法的,可以与另一个线程并行执行。

live example

答案 2 :(得分:0)

有一个线程安全的std::unordered_map实现是可能的(但通常不是很有用) - 问题是每个迭代器对象都需要锁定一个递归互斥锁,直到它的析构函数运行。这不仅会有点慢,而且迭代器会因内存使用而膨胀,还存在功能性问题:即使当它们不是“当前”时,保持迭代器也并不罕见。用于读取或写入容器(例如,对于某些二级索引,或作为&#34;游标&#34;,或者因为在使用它们之后,它们的销毁是懒惰的,直到封闭的范围退出或拥有对象被销毁):这意味着其他线程可能会被阻塞很长一段时间,实际上,容器操作周围的程序逻辑可能构成一种死锁。