我听说const 在C++11 中表示线程安全。这是真的吗?
有点是真的……
这就是标准语言对线程安全的看法:
[1.10/4]
如果其中一个修改了内存位置 (1.7) 而另一个访问或修改了相同的内存位置,则两个表达式求值冲突。
[1.10/21]
如果程序的执行在不同的线程中包含两个冲突的操作,则程序的执行包含一个数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生。任何此类数据竞争都会导致未定义的行为。
这不过是数据竞争发生的充分条件:
- 对给定事物同时执行两个或多个操作;和
- 其中至少有一个是写入。
标准库以此为基础,走得更远:
[17.6.5.9/1]
本节规定了实现应满足的要求,以防止数据竞争 (1.10)。除非另有说明,否则每个标准库函数都应满足每个要求。实施可能会在以下未指定的情况下防止数据竞争。
[17.6.5.9/3]
C++ 标准库函数不得直接或间接修改当前线程以外的线程可访问的对象 (1.10),除非通过函数的非const 参数直接或间接访问对象,包括this .
简单地说,它期望对const 对象的操作是线程安全的。这意味着 标准库 不会引入数据竞争,只要对您自己类型的 const 对象进行操作
- 完全由读取组成——也就是说,没有写入——;或
- 内部同步写入。
如果这种期望不适用于您的一种类型,那么直接或间接将其与标准库的任何组件一起使用可能会导致数据竞争。总之,从标准库的角度来看,const 确实意味着线程安全。重要的是要注意,这只是一个合同,编译器不会强制执行,如果你破坏它,你会得到未定义的行为,你只能靠你自己. const 是否存在不会影响代码生成——至少对于数据竞争而言不会——。
这是否意味着 const 现在等同于 Java 的 synchronized?
否。一点也不……
考虑以下表示矩形的过度简化的类:
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 可能最终仍会计算错误的值,因为对width 和height 的分配不受互斥体的保护。
如果我们真的想要一个线程安全 rect,我们将使用同步原语来保护非线程安全 rect。
他们的关键字用完了吗?
是的,他们是。从第一天开始,他们的关键字就用完了。
来源:You don't know const and mutable - Herb Sutter