在C ++ 11中安全吗?
我听说const
C ++ 11中的线程安全。 真的吗?
这是否意味着const
现在相当于Java的synchronized
?
他们用完了关键字吗?
我听说const
C ++ 11中的线程安全。 真的吗?
这是有点真...
这就是标准语言在线程安全方面所要说的:
[1.10 / 4]如果其中一个修改一个内存位置(1.7),另一个访问或修改相同的内存位置,则两个表达式评估冲突。
[1.10 / 21]一个程序的执行包含一个数据竞争,如果它包含两个不同线程中的冲突动作,其中至少有一个不是原子的,并且两个都不会发生在另一个线程之前。 任何这样的数据竞争都会导致未定义的行为。
这不是数据竞赛发生的充分条件:
标准库建立在此基础上,进一步:
[17.6.5.9/1]本节规定了实施应符合的防止数据竞争的要求(1.10)。 除非另有说明,否则每个标准库函数都应符合各项要求 在下面指定的情况以外的情况下,实现可能会阻止数据竞争。
[17.6.5.9/3] A 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
是在同一时间,一个呼叫发生set_size
在给定的rect
,然后area
最终可能会计算基于一个古老的宽度和一个新的高度的结果(甚至在乱码上)。
但是,这是rect
, rect
不是const
所以它甚至不希望是线程安全的。 另一方面,声明const rect
的对象是线程安全的,因为不可能写入(如果你正在考虑const_cast
最初声明为const
东西,那么你会得到未定义的行为,就是这样)。
那么它意味着什么呢?
我们假设 - 为了争论 - 乘法运算非常昂贵,我们尽可能避免它们。 我们只有在请求时才可以计算区域,然后将其缓存以防将来再次请求:
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
的调用可能会作为另一个对象的复制构造函数的一部分发生,这样的构造函数可能已经被标准容器上的某个操作调用,并且此时标准库期望这个操作表现为对数据的读取比赛。 但我们正在写作!
只要我们将标准容器rect
或间接地放置在标准容器中,我们即将与标准库签订合同。 为了继续在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
。
他们用完了关键字吗?
对,他们是。 自从第一天起,它们就已经用完了关键字。
来源:你不知道const
和mutable
- Herb Sutter
上一篇: safe in C++11?