2

GotW 文章 #45中,Herb 声明如下:

void String::AboutToModify(
  size_t n,
  bool   bMarkUnshareable /* = false */
) {
  if( data_->refs > 1 && data_->refs != Unshareable ) {
    /* ... etc. ... */

这个 if 条件不是线程安全的。一方面,评估甚至“data_->refs > 1”可能不是原子的;如果是这样,如果线程 1 尝试评估“data_->refs > 1”,而线程 2 正在更新 refs 的值,则从 data_->refs 读取的值可能是任何值——1、2,甚至是其他值这既不是原始值也不是新值。

此外,他指出 data_->refs 可以在与 1 比较和与 Unshareable 比较之间进行修改。

再往下,我们找到了一个解决方案:

void String::AboutToModify(
  size_t n,
  bool   bMarkUnshareable /* = false */
) {
  int refs = IntAtomicGet( data_->refs );
  if( refs > 1 && refs != Unshareable ) {
    /* ... etc. ...*/

现在,我知道两个比较都使用了相同的 refs,解决了问题 2。但为什么是 IntAtomicGet?我在该主题的搜索中一无所获——所有原子操作都集中在读、修改、写操作上,在这里我们只是读一下。那我们能不能...

int refs = data_->refs;

...最终可能只是一条指令?

4

2 回答 2

1

从共享内存 ( ) 中读取,data_->refs而另一个线程写入它是数据竞争的定义。

当我们非原子地读取data_->refs而另一个线程同时尝试写入时会发生什么?

假设线程 A 正在执行++data_->refs(写入),而线程 B 正在执行int x = data_->refs(读取)。想象一下,线程 B 从线程 B 读取前几个字节,并且线程 A在线程 B 完成读取之前data_->refs完成了其值的写入。data_->refs然后线程 B 在 处读取其余字节data_->refs

您既不会得到原始值,也不会得到新值;你会得到完全不同的价值!这个场景只是为了说明什么意思:

[...] 从 data_->refs 读取的值可能是任何值——1、2,甚至是既不是原始值也不是新值的值。

原子操作的目的是确保操作是不可分割的:它要么被观察为已完成,要么被观察为未完成。因此,我们使用原子读取操作来确保我们data_->refs在更新之前或之后(这取决于线程计时)获得值。

于 2015-10-20T21:35:26.407 回答
1

不同的平台对读/写操作的原子性做出不同的承诺。x86例如保证读取双字 ( 4 bytes) 将是原子操作。但是,您不能假设这对于任何架构都是正确的,而且很可能不会。

如果您计划将代码移植到不同的平台,这样的假设可能会给您带来麻烦并导致代码中出现奇怪的竞争条件。因此,最好保护自己并使读/写操作显式原子化。

于 2015-10-20T21:00:31.013 回答