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

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

【问题讨论】:

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

标签: c++ multithreading thread-safety


【解决方案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:: 我在回答中使用了地图,它可以很容易地被共享资源替换。是为了方便理解

      【讨论】:

        猜你喜欢
        • 2013-06-30
        • 2019-04-13
        • 2012-08-07
        • 2023-04-05
        • 2020-03-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多