【问题标题】:Does const mean thread-safe in C++11?const 是否意味着 C++11 中的线程安全?
【发布时间】:2012-12-17 03:14:01
【问题描述】:

我听说constC++11 中表示线程安全。这是真的吗?

这是否意味着const 现在等同于Javasynchronized

他们的关键字用完了吗?

【问题讨论】:

  • C++-faq 一般由 C++ 社区管理,您可以在我们的聊天中向我们提问。
  • @DeadMG:我不知道 C++-faq 及其礼仪,这是在评论中提出的。
  • 你从哪里听说 const 意味着线程安全?
  • @Mark B:Herb SutterBjarne StroustrupStandard C++ Foundation 上是这么说的,请参阅链接在答案的底部。
  • 请注意:真正的问题不是const 是否意味着线程安全。那将是无稽之谈,否则它这意味着您应该能够继续将每个线程安全方法标记为const。相反,我们真正要问的问题是const IMPLIES 线程安全,这就是本次讨论的内容。

标签: c++ c++11 thread-safety constants c++-faq


【解决方案1】:

我听说constC++11 中表示线程安全。这是真的吗?

有点是真的……

这就是标准语言对线程安全的看法:

[1.10/4] 如果其中一个修改了内存位置 (1.7) 而另一个访问或修改了相同的内存位置,则两个表达式求值冲突

[1.10/21] 如果程序的执行在不同的线程中包含两个冲突的操作,则程序的执行包含一个数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生。任何此类数据竞争都会导致未定义的行为。

这不过是数据竞争发生的充分条件:

  1. 对给定事物同时执行两个或多个操作;和
  2. 其中至少有一个是写入。

标准库以此为基础,走得更远:

[17.6.5.9/1] 本节规定了实现应满足的要求,以防止数据竞争 (1.10)。除非另有说明,否则每个标准库函数都应满足每个要求。实施可能会在以下未指定的情况下防止数据竞争。

[17.6.5.9/3] C++ 标准库函数不得直接或间接修改当前线程以外的线程可访问的对象 (1.10),除非通过函数的非const 参数直接或间接访问对象,包括this .

简单地说,它期望对const 对象的操作是线程安全的。这意味着 标准库 不会引入数据竞争,只要对您自己类型的 const 对象进行操作

  1. 完全由读取组成——也就是说,没有写入——;或
  2. 内部同步写入。

如果这种期望不适用于您的一种类型,那么直接或间接将其与标准库的任何组件一起使用可能会导致数据竞争。总之,从标准库的角度来看,const 确实意味着线程安全。重要的是要注意,这只是一个合同,编译器不会强制执行,如果你破坏它,你会得到未定义的行为,你只能靠你自己. const 是否存在不会影响代码生成——至少对于数据竞争而言不会——。

这是否意味着 const 现在等同于 Javasynchronized

。一点也不……

考虑以下表示矩形的过度简化的类:

class rect {
    int width = 0, height = 0;

public:
    /*...*/
    void set_size( int new_width, int new_height ) {
        width = new_width;
        height = new_height;
    }
    int area() const {
        return width * height;
    }
};

成员函数area线程安全的;不是因为它的const,而是因为它完全由读取操作组成。不涉及写入,并且至少需要涉及一次写入才能发生数据竞争。这意味着您可以从任意数量的线程中调用area,并且您将始终得到正确的结果。

请注意,这并不意味着rect线程安全的。事实上,很容易看出如果对area 的调用与在给定rect 上对set_size 的调用同时发生,那么area 最终可能会根据旧的计算结果宽度和新高度(甚至是乱码)。

不过没关系,rect 不是const,所以它甚至不期望是线程安全的。另一方面,声明为const rect 的对象将是线程安全的,因为不可能进行写入(如果您正在考虑const_cast-ing 最初声明为const 的东西,那么您会得到undefined-behavior 就是这样)。

那是什么意思呢?

让我们假设——为了争论——乘法运算非常昂贵,我们最好尽可能避免它们。我们可以仅在请求时计算区域,然后将其缓存以防将来再次请求:

class rect {
    int width = 0, height = 0;

    mutable int cached_area = 0;
    mutable bool cached_area_valid = true;

public:
    /*...*/
    void set_size( int new_width, int new_height ) {
        cached_area_valid = ( width == new_width && height == new_height );
        width = new_width;
        height = new_height;
    }
    int area() const {
        if( !cached_area_valid ) {
            cached_area = width;
            cached_area *= height;
            cached_area_valid = true;
        }
        return cached_area;
    }
};

[如果这个例子看起来太人为,你可以用一个非常大的动态分配整数替换int,它本质上是非线程安全的,并且对于它的乘法非常昂贵。]

成员函数area 不再是线程安全,它现在正在执行写入操作,并且没有在内部同步。这是个问题吗?对area 的调用可能作为另一个对象的copy-constructor 的一部分发生,这样的constructor 可能已被标准容器上的某些操作调用,此时 标准库 期望此操作在 数据竞争 方面表现为 读取。但我们正在写!

一旦我们将rect 放入标准容器——直接或间接——我们正在与标准库签订合同 /em>。为了继续在 const 函数中进行写入,同时仍然遵守该合同,我们需要在内部同步这些写入:

class rect {
    int width = 0, height = 0;

    mutable std::mutex cache_mutex;
    mutable int cached_area = 0;
    mutable bool cached_area_valid = true;

public:
    /*...*/
    void set_size( int new_width, int new_height ) {
        if( new_width != width || new_height != height )
        {
            std::lock_guard< std::mutex > guard( cache_mutex );
        
            cached_area_valid = false;
        }
        width = new_width;
        height = new_height;
    }
    int area() const {
        std::lock_guard< std::mutex > guard( cache_mutex );
        
        if( !cached_area_valid ) {
            cached_area = width;
            cached_area *= height;
            cached_area_valid = true;
        }
        return cached_area;
    }
};

请注意,我们创建了area 函数线程安全,但rect 仍然不是线程安全。在调用set_size 的同时调用area 可能最终仍会计算错误的值,因为对widthheight 的分配不受互斥体的保护。

如果我们真的想要一个线程安全 rect,我们将使用同步原语来保护非线程安全 rect

他们的关键字用完了吗?

是的,他们是。从第一天开始,他们的关键字就用完了。


来源You don't know const and mutable - Herb Sutter

【讨论】:

  • @Ben Voigt:据我了解,std::stringC++11 规范的措辞已经禁止 COW .不过具体的我不记得了……
  • @BenVoigt:不。它只会防止此类事情不同步——即,不是线程安全的。 C++11 已经明确禁止了 COW——不过,这个特定的段落与此无关,也不会禁止 COW。
  • 如果我没看错,重点是:“在const 函数中,您可以修改mutable 变量,只要您这样做原子地(即具有适当的同步)”。然而,[17.6.5.9/3] 说:[一个 STL 函数]“不得直接或间接修改”[其参数通过(除其他外)调用其const 成员函数之一] .但是由于那些const 成员函数可以修改mutable 变量,这意味着修改mutable 变量不会被视为根据[17.6.5.9/3] 的“修改”当您以原子方式进行操作时。而且我找不到说明的地方。
  • 在我看来存在逻辑上的差距。 [17.6.5.9/3] 禁止“过多”,说“不得直接或间接修改”;它应该说“不应直接或间接引入数据竞争”,除非原子写入在某处被定义为为“修改”。但我在任何地方都找不到这个。
  • 我可能在这里更清楚地说明了我的整个观点:isocpp.org/blog/2012/12/… 无论如何感谢您的帮助。
【解决方案2】:

这是对 K-ballo 答案的补充。

thread-safe 一词在此上下文中被滥用。正确的措辞是:const 函数意味着 thread-safe 按位 const 内部同步,如 Herb Sutter (29:43) 所述自己

同时从多个线程调用 const 函数应该是线程安全的,无需在另一个线程中同时调用 non-const 函数。

因此,一个 const 函数不应该(并且在大多数情况下也不会)是真正的线程安全的,因为它可能会读取可能被另一个 非 const 更改的内存(没有内部同步) 功能。一般来说,这不是线程安全的,因为即使只有一个线程正在写入(而另一个线程正在读取数据)也会发生数据竞争。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-08
    • 2017-05-14
    • 1970-01-01
    相关资源
    最近更新 更多