C++ 线程安全 - 地图读取

Posted

技术标签:

【中文标题】C++ 线程安全 - 地图读取【英文标题】:C++ thread safety - map reading 【发布时间】:2016-03-12 15:53:25 【问题描述】:

我正在开发一个需要 std::map 的程序,特别是像 map<string,map<string,int>> 这样的程序 - 它类似于银行兑换率 - 第一个字符串是原始货币,第二个地图中的一个是想要的,int 是他们的费率。整个地图将是只读。我还需要互斥锁吗?我对整个线程的安全性有点困惑,因为这是我的第一个更大的多线程程序。

【问题讨论】:

我相信如果您的map 在任何线程访问它之前将其所有元素都插入其中,那么您就可以了。 这张地图是如何填充的?在编译时?在读取踏步开始之前的运行时?在运行时与读取线程同时进行? 【参考方案1】:

如果您谈论的是标准 std::map 并且没有线程写入它,则不需要同步。没有写入的并发读取很好。

但是,如果至少有一个线程在映射上执行写入操作,那么您确实需要某种保护,例如互斥锁。

请注意 std::map::operator[] 算作写入,因此请改用 std::map::at(或 std::map::find,如果该键可能不存在于映射中)。您可以通过const map& 仅引用共享映射来使编译器保护您免受意外写入。


在 OP 中被澄清为这种情况。为了完整起见:请注意,其他类可能有 mutable 成员。对于那些人来说,即使通过const& 访问也可能会引发一场比赛。如果有疑问,请查看文档或使用其他东西进行并行编程。

【讨论】:

好的,感谢操作员提示,这将派上用场。顺便说一句,为什么 operator[] 会这样工作? @Jesse_Pinkman 如果找不到则插入元素,这显然会修改地图。【参考方案2】:

经验法则是,如果您有共享数据并且至少有一个线程是写入器,那么您需要同步。如果其中一个线程是写入器,则您必须进行同步,因为您不希望读取器读取正在写入的元素。这可能会导致问题,因为读者可能会读取部分旧值和部分新值。

在您的情况下,由于所有线程都只会读取数据,因此它们无能为力会影响映射,因此您可以进行并发(非同步)读取。

【讨论】:

【参考方案3】:

std::map<std::string, std::map<std::string,int>> const 包装在一个只有const 成员函数的自定义类中[*]

这将确保所有在创建类对象后使用该类对象的线程只会从中读取,从 C++11 开始保证是安全的。

正如documentation 所说:

所有const的成员函数都可以被不同的人同时调用 同一容器上的线程。

无论如何,用您自己的自定义类型包装容器是一种很好的做法。提高线程安全性只是这种良好做法的一个积极副作用。其他积极影响包括增加客户端代码的可读性、减少/适应所需功能的容器接口、易于添加额外的约束和检查。

这是一个简单的例子:

class BankChangeRates

public:

    BankChangeRates(std::map<std::string, std::map<std::string,int>> const& data) : data(data) 

    int get(std::string const& key, std::string const& inner_key) const
    
        auto const find_iter = data.find(key);
        if (find_iter != data.end())
        
            auto const inner_find_iter = find_iter->second.find(inner_key);
            if (inner_find_iter != find_iter->second.end())
            
                return inner_find_iter->second;
            
        
        // error handling
    

    int size() const
    
        return data.size();
    

private:
    std::map<std::string, std::map<std::string,int>> const data;
;

在任何情况下,线程安全问题都归结为如何确保构造函数不会从另一个线程写入的对象中读取。这通常是微不足道的。例如,可以在多线程开始之前构建对象,或者可以使用硬编码的初始化列表对其进行初始化。在许多其他情况下,创建对象的代码通常只会访问其他线程安全函数和本地对象。

重点是对象的并发访问在创建后始终是安全的。


[*] 当然,const 成员函数应该信守承诺,不要尝试使用mutableconst_cast 的“变通方法”。

【讨论】:

【参考方案4】:

如果您完全确定这两个映射始终是只读的,那么您永远不需要互斥锁。

但是您必须格外小心,在程序执行期间没有人可以通过任何方式更新地图。确保您在程序的初始化阶段初始化地图,然后永远不要以任何理由更新它。

如果您对此感到困惑,将来您可能需要在程序执行之间对其进行更新,那么最好在地图周围放置宏,这些宏现在是空的。将来,如果您需要围绕它们的互斥体,只需更改宏定义即可。

PS:: 我在回答中使用了地图,它可以很容易地被共享资源替换。这是为了便于理解

【讨论】:

以上是关于C++ 线程安全 - 地图读取的主要内容,如果未能解决你的问题,请参考以下文章

C++ 多线程 同时读取同一个vector 线程安全 吗

C++ 多线程 同时读取同一个vector 线程安全 吗

C++ 标准容器的线程安全

C++ 线程安全对象缓存的设计选项

Visual C++ 2010 中的 STL 映射实现和线程安全

跨多个线程和 cpu 安全地修改和读取布尔值的选项都有哪些?