让我们考虑以下C ++中的两线程并发程序:
x,y
是全局变量,r1,r2
是线程局部的,store
和load
至int
是原子的。
内存模型= C ++ 11
int x = 0, int y = 0
r1 = x | r2 = y
y = r1 | x = r2
允许编译器将其编译为:
int x = 0, int y = 0
r1 = x | r2 = 42
y = r1 | x = r2
| if(y != 42)
| x = r2 = y
而且,尽管它是线程内一致的,但它可能导致错误的结果,因为该程序的执行可能导致(x, y) = (42, 42)
这被称为 Out of Thin Air 值问题。它存在,我们必须忍受这一点。
我的问题是:内存壁垒是否会阻止编译器进行疯狂的优化,从而导致超值?
例如:
[fence] = atomic_thread_fence(memory_order_seq_cst);
int x = 0, int y = 0
r1 = x | r2 = y
[fence] | [fence]
y = r1 | x = r2
答案 0 :(得分:1)
您在x
和y
上存在数据争用未定义行为,因为它们不是atomic
变量,因此C ++ 11标准对于允许的内容绝对无话可说发生。
在没有正式内存模型的情况下针对较旧的语言标准进行研究很重要,因为人们还是使用volatile
或纯int
以及编译器+ asm屏障进行了线程化,而行为可能取决于编译器在这种情况下按预期方式工作。但是幸运的是,“在当前实现上进行工作的可能性”线程的糟糕时光已经过去。
这里的障碍没有帮助,没有任何东西可以创建同步。正如@davmac解释的那样,没有任何障碍要求在全局操作顺序中“排队”。 将屏障视为使当前线程等待其部分或全部先前操作成为全局可见的操作;障碍不会直接与其他线程交互。
凭空获取的值是由于未定义的行为而可能发生的一件事;允许编译器对非原子变量进行软件值预测,并发明对肯定会写入的对象的写入。如果存在发布存储区或宽松存储区+障碍,则可能不允许编译器在其之前发明写操作,因为可以创建
通常,从C ++ 11语言律师的角度来看,您无法采取任何措施来确保程序安全(除了互斥锁或原子原子的手动锁定之外,以防止一个线程在读取时x
另一个正在写。)
如果您指望该变量的其他用途得到积极优化,则也许可以击败自动向量化和东西。
atomic_int x = 0, y = 0
r1 = x.load(mo_relaxed) | r2 = y.load(mo_relaxed)
y.store(r1, mo_relaxed) | x.store(r2, mo_relaxed)
在线程2从r2
看到值之前,值预测可以推测性地将y
的未来值输入到管道中,但是直到其他软件或硬件才能真正看到该值肯定知道预测是正确的。 (那会发明一种写方法。)
例如线程2被允许编译为
r2 = y.load(mo_relaxed);
if (r2 == 42) { // control dependency, not a data dependency
x.store(42, mo_relaxed);
} else {
x.store(r2, mo_relaxed);
}
但是正如我所说的,x = 42;
在非推测性(硬件或软件推测)之前不会对其他线程可见,因此值预测无法发明其他线程可以看到的值。 C ++ 11标准保证了原子
我不知道/想不出任何机制,在42
实际看到42之前,其他线程实际上可以看到y.load
的存储。(即LoadStore的重新排序更高版本的从属存储的加载)。不过,我认为C ++标准并不能正式保证这一点。如果编译器可以证明r2
在某些情况下始终为42,甚至消除控件依赖关系,那么也许真的是积极的线程间优化?
获取负载或发布存储绝对足以阻止因果关系违规。这不是mo_consume
,因为r2
用作值,而不是指针。
答案 1 :(得分:0)
本身不是。在您的示例中,没有任何同步两个线程的操作。特别是,两个线程中的隔离都不会导致线程在该点同步;例如,您可能会获得以下顺序:
(Thread #1) | (Thread #2)
r1 = x |
[fence] |
y = junk temporary |
| r2 = y // junk!
| [fence]
| x = r2
y = r1 |
避免出现空中结果的最简单方法是使用原子整数:如果x和y原子为原子,则它们不能具有“空中”值:
std::atomic_int x = 0, y = 0;
int r1 = x; | int r2 = y;
y = r1; | x = r2;