C++ 多线程无序映射

C++ 多线程无序映射,c++,multithreading,c++11,hashtable,c++14,c++17,C++,Multithreading,C++11,Hashtable,C++14,C++17,我在一个多线程环境中工作。基本上,我有一个unordered_map,它可以被多个线程同时访问。现在,我的算法是: function foo(key) { scoped_lock() if key exists { return Map[key] } value = get_value() Map[key] = value } 显然,这种实现的性能并不好。是否有任何算法/方法可用于提高性能 编辑: 我做了很多测试,并考虑了双重检查锁定。因此,我用以下内容修改了代码

我在一个多线程环境中工作。基本上,我有一个
unordered_map
,它可以被多个线程同时访问。现在,我的算法是:

function foo(key) {
  scoped_lock()
  if key exists {
    return Map[key]
  }

  value = get_value()
  Map[key] = value
}
显然,这种实现的性能并不好。是否有任何算法/方法可用于提高性能

编辑:

我做了很多测试,并考虑了双重检查锁定。因此,我用以下内容修改了代码:

function foo(key) {
  if key exists {
    return Map[key]
  }

  scoped_lock()
  if key exists {
    return Map[key]
  }

  value = get_value()
  Map[key] = value
}

实际上,我只在作用域为的_lock()之前添加了另一个检查。在此场景中,假设函数被调用
N次
。如果第一个
m
调用
foo
,其中
m
,填充了映射,下一个
N-m
调用只从映射中获取值,我不需要独占访问。此外,在
作用域锁定
之后还有另一项检查,以确保线程安全。我说得对吗?在任何情况下,第一个代码的执行需要208s左右,而第二个代码的执行需要200s左右。

下面是一个实用程序类:

template<class T, class M=std::mutex, template<class...>class S=std::unique_lock, template<class...>class U=std::unique_lock>
struct mutex_protected {
  template<class F>
  auto read( F&& f ) const
  -> typename std::result_of<F&&(T const&)>::type
  {
    auto l = lock();
    return std::forward<F>(f)(data);
  }
  template<class F>
  auto write( F&& f ) 
  -> typename std::result_of<F&&(T&)>::type
  {
    auto l = lock();
    return std::forward<F>(f)(data);
  }
  mutex_protected(mutex_protected&&)=delete;
  mutex_protected& operator=(mutex_protected&&)=delete;

  template<class...Args>
  mutex_protected( Args&&...args ):
    data( std::forward<Args>(args)... )
  {}
private:
  mutable M m;
  T data;

  U<M> lock() { return U<M>(m); }
  S<M> lock() const { return S<M>(m); }
};
这使得能够同时拥有多个读卡器。但是您应该首先确定简单的互斥锁是否足够快

struct cache {
  using Key=std::string;
  using Value=int;
  using Map=std::unordered_map< Key, Value >;
  Value get( Key const& k ) {
    Value* r = table.read([&](Map const& m)->Value*{
      auto it = m.find(k);
      if (it == m.end()) return nullptr;
      return std::addressof( it->second );
    });
    if (r) return *r;
    return table.write([&](Map& m)->Value{
      auto it = m.find(k);
      if (it != m.end()) return it->second;
      auto r = m.insert( std::make_pair(k, 42) ); // construct data here
      return r.first->second;
    });
  }
private:
  mutex_guarded< std::unordered_map< Key, Value > > table;
};
struct缓存{
使用Key=std::string;
使用Value=int;
使用Map=std::无序_Map;
获取值(关键常量和k){
Value*r=table.read([&](Map const&m)->Value*{
自动it=m.find(k);
if(it==m.end())返回nullptr;
返回std::addressof(it->second);
});
如果(r)返回*r;
返回表.write([&](Map&m)->值{
自动it=m.find(k);
如果(it!=m.end())返回它->秒;
auto r=m.insert(std::make_pair(k,42));//在此处构造数据
返回r.first->second;
});
}
私人:
互斥锁受保护>表;
};
升级
mutex\u-guarded
rw\u-guarded
并切换至读写器锁


下面是一个更复杂的版本:

有两张地图;一个是为了价值,一个是为了共享价值的未来

使用读写器锁(也称为共享互斥锁)

若要获取,请获取共享锁。检查它是否在那里。如果是,请返回

解锁第一张地图。锁定第二个映射以进行写入。如果密钥下没有共享的未来,请添加一个。解锁地图2,并等待共享未来,无论您是否添加了它

完成后,锁定第一张地图以便阅读;检查结果是否已经存在。如果是,请退回。如果没有,解锁,重新锁定写入,将数据移动到地图1中,如果还没有,返回第一个地图中的数据

这是为了最大限度地减少周期映射1被锁定为独占,从而允许最大并发性

其他设计将优化其他考虑因素


不要使用
运算符[]
。如果没有某种类型的锁处于活动状态,请不要与任何贴图交互。知道哪些锁对应于哪个地图。请注意,在某些情况下,读取元素(不查找)可以在没有锁定的情况下完成。有时需要阅读共享内容的副本,而不是共享内容。查找每种类型的文档,以确定哪些操作需要哪些锁。

下面是一个实用程序类:

template<class T, class M=std::mutex, template<class...>class S=std::unique_lock, template<class...>class U=std::unique_lock>
struct mutex_protected {
  template<class F>
  auto read( F&& f ) const
  -> typename std::result_of<F&&(T const&)>::type
  {
    auto l = lock();
    return std::forward<F>(f)(data);
  }
  template<class F>
  auto write( F&& f ) 
  -> typename std::result_of<F&&(T&)>::type
  {
    auto l = lock();
    return std::forward<F>(f)(data);
  }
  mutex_protected(mutex_protected&&)=delete;
  mutex_protected& operator=(mutex_protected&&)=delete;

  template<class...Args>
  mutex_protected( Args&&...args ):
    data( std::forward<Args>(args)... )
  {}
private:
  mutable M m;
  T data;

  U<M> lock() { return U<M>(m); }
  S<M> lock() const { return S<M>(m); }
};
这使得能够同时拥有多个读卡器。但是您应该首先确定简单的互斥锁是否足够快

struct cache {
  using Key=std::string;
  using Value=int;
  using Map=std::unordered_map< Key, Value >;
  Value get( Key const& k ) {
    Value* r = table.read([&](Map const& m)->Value*{
      auto it = m.find(k);
      if (it == m.end()) return nullptr;
      return std::addressof( it->second );
    });
    if (r) return *r;
    return table.write([&](Map& m)->Value{
      auto it = m.find(k);
      if (it != m.end()) return it->second;
      auto r = m.insert( std::make_pair(k, 42) ); // construct data here
      return r.first->second;
    });
  }
private:
  mutex_guarded< std::unordered_map< Key, Value > > table;
};
struct缓存{
使用Key=std::string;
使用Value=int;
使用Map=std::无序_Map;
获取值(关键常量和k){
Value*r=table.read([&](Map const&m)->Value*{
自动it=m.find(k);
if(it==m.end())返回nullptr;
返回std::addressof(it->second);
});
如果(r)返回*r;
返回表.write([&](Map&m)->值{
自动it=m.find(k);
如果(it!=m.end())返回它->秒;
auto r=m.insert(std::make_pair(k,42));//在此处构造数据
返回r.first->second;
});
}
私人:
互斥锁受保护>表;
};
升级
mutex\u-guarded
rw\u-guarded
并切换至读写器锁


下面是一个更复杂的版本:

有两张地图;一个是为了价值,一个是为了共享价值的未来

使用读写器锁(也称为共享互斥锁)

若要获取,请获取共享锁。检查它是否在那里。如果是,请返回

解锁第一张地图。锁定第二个映射以进行写入。如果密钥下没有共享的未来,请添加一个。解锁地图2,并等待共享未来,无论您是否添加了它

完成后,锁定第一张地图以便阅读;检查结果是否已经存在。如果是,请退回。如果没有,解锁,重新锁定写入,将数据移动到地图1中,如果还没有,返回第一个地图中的数据

这是为了最大限度地减少周期映射1被锁定为独占,从而允许最大并发性

其他设计将优化其他考虑因素


不要使用
运算符[]
。如果没有某种类型的锁处于活动状态,请不要与任何贴图交互。知道哪些锁对应于哪个地图。请注意,在某些情况下,读取元素(不查找)可以在没有锁定的情况下完成。有时需要阅读共享内容的副本,而不是共享内容。查找每种类型的文档,以确定哪些操作需要哪些锁。

可能重复的@orhtej2可能重复的副本我正在寻找性能!查看@orhtej2的可能副本我正在寻找性能!谢谢你的回复。这个主意听起来不错,即使我不完全理解。我想知道是否有一种方法我可以遵循。我使用了第二种方法。您对此有何看法?@surcle由于缺乏同步,它会导致未定义的行为。“不,这是我拼凑的东西。”。SRW锁是标准方法;我的修改是假设创建对象的成本是中等的,并且您不能在这样做时阻止其他线程。如果创建成本低,则存在更简单的解决方案。@Surcle不,它不是。T