【问题标题】:unordered_map thread safetyunordered_map 线程安全
【发布时间】:2012-03-29 22:33:06
【问题描述】:

我正在使用 boost:thread 库将单线程程序更改为多线程。该程序使用 unordered_map 作为 hasp_map 进行查找。我的问题是..

在某个时间,许多线程将在写入,而在另一时间,许多线程将在读取,但不会同时读取和写入,即要么所有线程都在读取,要么所有线程都在写入。那会是线程安全的并且是为此设计的容器吗?如果会,它真的会并发并提高性能吗?我需要使用一些锁定机制吗?

我在某处读到 C++ 标准说行为是未定义的,但仅此而已吗?

更新:我也在考虑 Intel concurrent_hash_map。这会是一个不错的选择吗?

【问题讨论】:

  • "但就是这样"...作为 UB 应该足以不这样做。
  • @PlasmaHH 如果它是特定于实现的,如果您想为该实现者编写代码,它可能是。如果你为 Windows 编写代码,微软实现某些东西的方式对你来说可能就足够了。
  • 阅读“行为将未定义”,因为它不起作用。
  • 回复:concurrent_hash_map,我用过,效果很好。见full sample code

标签: c++ multithreading boost thread-safety hashmap


【解决方案1】:

STL 容器的设计保证您能够拥有:

A.多个线程同时读取

B.一个线程同时写

多线程写入不属于上述条件之一,是不允许的。多个线程写入将因此产生数据竞争,这是未定义的行为。

您可以使用互斥锁来解决此问题。 shared_mutex(与 shared_locks 结合使用)将特别有用,因为这种类型的互斥锁允许多个并发读取器。

http://eel.is/c++draft/res.on.data.races#3 是标准的一部分,它保证了在不同线程上同时使用 const 函数的能力。 http://eel.is/c++draft/container.requirements.dataraces 指定了一些额外的非常量操作,这些操作在不同线程上是安全的。

【讨论】:

  • @EthanSteinberg- std::mutex?或 boost:mutex?
  • @questions 他们都是一样的。 Boost::mutex 可能比 std::mutex 更容易使用,因为有更多的编译器支持它。
  • @EthanSteinberg- 我不需要锁定读者,对吧?因为多个线程可以同时读取。
  • 你有说法的来源吗:保证多个线程能够同时读取?
  • 澄清我不能从一个线程读取并同时从另一个线程写入?
【解决方案2】:

std::unordered_map 符合 Container 的要求(参考 http://en.cppreference.com/w/cpp/container/unordered_map)。有关容器线程安全,请参阅:http://en.cppreference.com/w/cpp/container#Thread_safety

要点:

  • “同一个容器中的不同元素可以被不同的线程同时修改”
  • "所有的const成员函数都可以被同一个容器上的不同线程并发调用。另外,成员函数begin()、end()、rbegin()、rend()、front()、back(), data()、find()、lower_bound()、upper_bound()、equal_range()、at() 以及,除了在关联容器中,operator[] 出于线程安全的目的而表现得像 const(也就是说,它们可以也可以被同一个容器上的不同线程同时调用)。”

【讨论】:

    【解决方案3】:

    这将是线程安全的并且是为此设计的容器吗?

    不,标准容器不是线程安全的。

    我需要使用一些锁定机制吗?

    是的,你知道。由于您使用的是 boost,boost::mutex 是个好主意;在 C++11 中,有 std::mutex

    我在某处读到 C++ 标准说行为是未定义的,但仅此而已吗?

    确实,行为是未定义的。我不确定您所说的“就是这样吗?”,因为未定义的行为是最糟糕的一种行为,而表现出这种行为的程序根据定义是不正确的。特别是,不正确的线程同步可能会导致随机崩溃和数据损坏,而且通常以非常难以诊断的方式发生,因此您最好不惜一切代价避免它。

    更新:我也在考虑 Intel concurrent_hash_map。这会是一个不错的选择吗?

    听起来不错,但我自己从未使用过,所以我无法提供意见。

    【讨论】:

    【解决方案4】:

    现有答案涵盖了要点:

    • 您必须拥有锁才能读取或写入地图
    • 您可以使用多读/单写锁来提高并发性

    另外,您应该注意:

    • 使用较早检索的迭代器,或指向映射中项目的引用或指针,计为读取或写入操作

    • 在其他线程中执行的写入操作可能会使映射中的指针/引用/迭代器无效,就像它们在同一个线程中完成时一样,即使在尝试继续之前再次获取了锁使用它们...

    【讨论】:

      【解决方案5】:

      您可以在访问 unordered_map 时使用 concurrent_hash_map 或使用互斥锁。使用 intel concurrent_hash_map 的问题之一是您必须包含 TBB,但您已经使用了 boost.thread。这两个组件具有重叠的功能,因此会使您的代码库复杂化。

      【讨论】:

        【解决方案6】:

        std::unordered_map 非常适合某些多线程情况。

        还有other concurrent maps from Intel TBB

        • tbb:concurrent_hash_map。它支持用于插入/更新的细粒度的每个键锁定,这是其他哈希映射无法提供的。但是,语法略显冗长。见full sample code。推荐。
        • tbb:concurrent_unordered_map。它本质上是相同的东西,一个键/值映射。但是,它的级别要低得多,并且更难使用。必须提供一个散列器、一个相等运算符和一个分配器。任何地方都没有示例代码,即使在英特尔官方文档中也是如此。不推荐。

        【讨论】:

          【解决方案7】:

          如果您不需要 unordered_map 的所有功能,那么此解决方案应该适合您。它使用互斥锁来控制对内部 unordered_map 的访问。该解决方案支持以下方法,添加更多应该相当容易:

          • getOrDefault(key, defaultValue) - 返回与 key 关联的值,如果不存在关联则返回 defaultValue。当不存在关联时,创建地图条目。
          • contains(key) - 返回一个布尔值;如果关联存在,则为 true。
          • put(key, value) - 创建或替换关联。
          • remove(key) - 删除键的关联
          • associations() - 返回一个包含所有当前已知关联的向量。

          示例用法:

          /* SynchronizedMap
          ** Functional Test
          ** g++ -O2 -Wall -std=c++11 test.cpp -o test
          */
          #include <iostream>
          #include "SynchronizedMap.h"
          
          using namespace std;
          using namespace Synchronized;
          
          int main(int argc, char **argv) {
          
              SynchronizedMap<int, string> activeAssociations;
          
              activeAssociations.put({101, "red"});
              activeAssociations.put({102, "blue"});
              activeAssociations.put({102, "green"});
              activeAssociations.put({104, "purple"});
              activeAssociations.put({105, "yellow"});
              activeAssociations.remove(104);
          
              cout << ".getOrDefault(102)=" << activeAssociations.getOrDefault(102, "unknown") << "\n";
              cout << ".getOrDefault(112)=" << activeAssociations.getOrDefault(112, "unknown") << "\n";
          
              if (!activeAssociations.contains(104)) {
                  cout << 123 << " does not exist\n";
              }
              if (activeAssociations.contains(101)) {
                  cout << 101 << " exists\n";
              }
          
              cout << "--- associations: --\n";
              for (auto e: activeAssociations.associations()) {
                  cout << e.first << "=" << e.second << "\n";
              }
          }
          

          样本输出:

          .getOrDefault(102)=green
          .getOrDefault(112)=unknown
          123 does not exist
          101 exists
          --- associations: --
          105=yellow
          102=green
          101=red
          

          注意 1:associations() 方法不适用于非常大的数据集,因为它会在创建向量列表期间锁定表。

          注意 2:我故意不扩展 unordered_map,以防止我自己意外使用 unordered_map 中尚未扩展的方法,因此可能不是线程安全的。

          #pragma once
          /*
           * SynchronizedMap.h
           * Wade Ryan 20200926
           * c++11
           */
          
          #include <unordered_map>
          #include <mutex>
          #include <vector>
          
          #ifndef __SynchronizedMap__
          #define __SynchronizedMap__
          
          using namespace std;
          
          
          namespace Synchronized {
          
              template <typename KeyType, typename ValueType>
          
              class SynchronizedMap {
          
              private:
                  mutex sync;
                  unordered_map<KeyType, ValueType> usermap;    
          
              public:
                  ValueType getOrDefault(KeyType key, ValueType defaultValue) {
                      sync.lock();
          
                      ValueType rs;
          
                      auto value=usermap.find(key);
                      if (value == usermap.end()) {
                          rs = defaultValue;
                      } else {
                          rs = value->second;
                      }
                      sync.unlock();
                      return rs;
                  }
          
                  bool contains(KeyType key) {
                      sync.lock();
                      bool exists = (usermap.find(key) != usermap.end());
                      sync.unlock();
                      return exists;
                  }
          
                  void put(pair<KeyType, ValueType> nodePair) {
                      sync.lock();
          
                      if (usermap.find(nodePair.first) != usermap.end()) {
                          usermap.erase(nodePair.first);
                      }
                      usermap.insert(nodePair);
                      sync.unlock();
                  }
          
                  void remove(KeyType key) {
                      sync.lock();
          
                      if (usermap.find(key) != usermap.end()) {
                          usermap.erase(key);
                      }
                      sync.unlock();
                  }
          
                  vector<pair<KeyType, ValueType>> associations() {
                      sync.lock();
           
                      vector<pair<KeyType, ValueType>> elements;
          
                      for (auto it=usermap.begin(); it != usermap.end(); ++it) {
                          pair<KeyType, ValueType> element (it->first, it->second);
                          elements.push_back( element );
                      }
          
                      sync.unlock();
                      return elements;
                  }
              };       
          }
          
          #endif
          

          【讨论】:

          • 不要把“使用命名空间标准;”在头文件中,它弄乱了命名空间。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-08-20
          • 1970-01-01
          相关资源
          最近更新 更多