(嗯……但不要问我这到底是什么意思)
我会尝试一下。
需要记住的是,编译器或 CPU 本身可能会重新排序内存读取和写入,如果它们看起来不能相互处理。
这很有用,例如,如果您有一些代码可能正在更新结构:
if ( playerMoved ) {
playerPos.X += dx;
playerPos.Y += dy;
// Keep the player above the world's surface.
if ( playerPos.Z + dz > 0 ) {
playerPos.Z += dz;
}
else {
playerPos.Z = 0;
}
}
上述大多数语句可能会被重新排序,因为它们之间没有数据依赖关系,事实上,超标量 CPU 可能会同时执行大多数这些语句,或者可能会更早开始处理 Z 部分,因为它不会影响 X或 Y,但可能需要更长时间。
这就是问题所在 - 假设您正在尝试无锁编程。你想执行一大堆内存写入,也许,填充一个共享队列。你通过最终写入一个标志来表示你已经完成了。
好吧,由于该标志似乎与正在完成的其余工作无关,编译器和 CPU 可能会重新排序这些指令,现在您可以在实际提交之前设置“完成”标志结构的其余部分到内存中,现在您的“无锁”队列不起作用。
这就是 Acquire 和 Release 排序语义发挥作用的地方。我通过设置一个带有 Acquire 语义的标志来设置我正在工作,并且 CPU 保证我在该指令之后玩的任何记忆游戏实际上都保持在该指令之下。我通过设置一个带有 Release 语义的标志来设置我已经完成了,CPU 保证我在发布之前所做的任何记忆游戏实际上都保留在发布之前。
通常,人们会使用显式锁来执行此操作 - 互斥锁、信号量等,其中 CPU 已经知道它必须注意内存排序。尝试创建“无锁”数据结构的目的是提供线程安全的数据结构(对于线程安全的某些含义),不使用显式锁(因为它们非常慢)。
可以在不支持获取/释放排序语义的 CPU 或编译器上创建无锁数据结构,但这通常意味着使用了一些较慢的内存排序语义。例如,您可以发出一个完整的内存屏障——该指令之前的所有内容都必须在该指令之前实际提交,该指令之后的所有内容都必须在该指令之后实际提交。但这可能意味着我在指令流的前面等待一堆实际上不相关的内存写入(可能是函数调用序言),这与我试图实现的内存安全无关。
Acquire 说“只担心我之后的事情”。发布说“只担心我面前的事情”。将这两者结合起来就是一个完整的内存屏障。