unordered_map 线程安全性

43

我正在使用boost:thread库将单线程程序转换为多线程程序。该程序使用unordered_map作为哈希表进行查找。我的问题是...

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

我曾经阅读过某篇文章,其中提到C++标准表示行为将是未定义的,但这就是全部吗?

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


7
“但是这就是全部吗”……做为 UB 应该足以不去做它。 - PlasmaHH
阅读此链接:https://dev59.com/H3M_5IYBdhLWcg3wfTE0 - xanatos
如果它是特定于实现的,那么它可能是,如果你想为该实现者编写代码的话。如果你为Windows编写代码,Microsoft实现某些东西的方式可能已经足够了。 - xanatos
2
将“行为未定义”理解为“无法工作”。 - brian beuning
关于concurrent_hash_map,我已经使用过它并且效果很好。请参考完整示例代码 - Contango
7个回答

70

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 指定了一些其他非const操作,在不同线程上是安全的。


@EthanSteinberg- std::mutex?还是boost:mutex? - questions
@questions 它们都是一样的。Boost::mutex 可能会更容易使用,因为比 std::mutex 支持更多的编译器。 - user406009
3
你是否有这个断言的来源:多线程保证能够同时读取? - DrYap
4
我不能同时从一个线程读取并从另一个线程写入,这是需要澄清的。 - Oleg Vazhnev
@MihaiDanila 嗯,它们并不是线程安全的,因为您不能同时拥有多个编写器和/或读者。但是,您可以同时拥有多个读者。请参见 http://eel.is/c++draft/res.on.data.races#3 了解标准库在您使用 const(只读函数)时不允许引起竞争的方法。 http://eel.is/c++draft/container.requirements.dataraces 还添加了一些额外的保证,使您甚至可以安全地执行一些“非const”操作。 - user406009
显示剩余2条评论

21

std::unordered_map满足容器需求(参见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(也就是说,它们也可以被不同的线程同时调用)。”

11

它是线程安全的,容器是为此设计的吗?

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

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

是的,你需要。因为你正在使用 boost,boost::mutex 是一个不错的选择;在 C++11 中,有 std::mutex

我在某个地方读到 C++ 标准说行为将是未定义的,但这就是全部吗?

确实,行为是未定义的。我不确定你所指的意思是什么:“这就是全部?” 由于未定义的行为是最糟糕的行为,而展现出它的程序从定义上就是不正确的。特别是,不正确的线程同步很可能导致随机崩溃和数据损坏,通常以非常难以诊断的方式发生,因此你应该尽力避免它。

更新:我还考虑过 Intel concurrent_hash_map。那会是一个好选择吗?

听起来不错,但我自己没有使用过,无法发表意见。


关于concurrent_hash_map,我已经使用过它并且效果很好。请参考完整示例代码 - Contango

3
现有的答案覆盖了主要观点:
  • 您必须拥有锁才能读取或写入地图
  • 您可以使用多读单写锁来提高并发性
此外,您应该知道:
  • 使用早先检索的迭代器或指向地图中项目的引用或指针都算作读取或写入操作

  • 在其他线程中执行的写操作可能会使指向地图的指针/引用/迭代器失效,就像它们在同一线程中执行一样,即使在继续使用它们之前再次获取锁定也是如此...


1

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


1

std::unordered_map适用于一些多线程情况。

此外,还有来自Intel TBB的其他并发映射

  • tbb:concurrent_hash_map。它支持细粒度的、每个键的插入/更新锁定,这是很少有其他哈希映射可以提供的。但是,语法稍微有点啰嗦。请参见完整示例代码。推荐使用。
  • tbb:concurrent_unordered_map。它本质上是相同的,一个键/值映射。然而,它更低级别,更难使用。必须提供一个哈希器、一个相等运算符和一个分配器。甚至在官方Intel文档中也没有示例代码。不推荐使用。

0

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

  • getOrDefault(key, defaultValue) - 返回与key相关联的值,如果不存在关联,则返回defaultValue。 当不存在关联时,不会创建映射条目。
  • contains(key) - 返回布尔值; 如果存在关联则为true。
  • put(key, value) - 创建或替换关联。
  • remove(key) - 删除与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

1
不要在头文件中放置“using namespace std;”,它会干扰命名空间。 - 0xAA55

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接