use*_*112 2 c++ cpu concurrency x86 multithreading
在我知道 CPU 的存储缓冲区之前,我认为线程抖动只是在两个线程想要写入同一个缓存行时发生。一个会阻止另一个写作。然而,这似乎非常同步。后来我了解到有一个存储缓冲区,可以临时刷新写入。它被迫通过 SFENCE 指令刷新,有点暗示没有同步阻止多个内核访问同一缓存行....
如果我们必须小心并使用 SFENCE,我完全困惑线程抖动是如何发生的?线程抖动意味着阻塞,而 SFENCE 意味着写入是异步完成的,程序员必须手动刷新写入?
(我对 SFENCE 的理解也可能会混淆——因为我也读过英特尔内存模型是“强”的,因此只有字符串 x86 指令才需要内存栅栏)。
有人可以消除我的困惑吗?
“抖动”意味着多个内核检索相同的 cpu 缓存线,这会导致其他内核竞争同一缓存线的延迟开销。
所以,至少在我的词汇中,当你有这样的事情时会发生线程颠簸:
// global variable
int x;
// Thread 1
void thread1_code()
{
while(!done)
x++;
}
// Thread 2
void thread2_code()
{
while(!done)
x++;
}
Run Code Online (Sandbox Code Playgroud)
(这段代码当然完全是胡说八道——我让它变得非常简单,但没有复杂的代码来解释线程本身发生的事情是没有意义的)
为简单起见,我们假设线程 1 始终在处理器 1 上运行,线程 2 始终在处理器 2 上运行 [1]
如果您在 SMP 系统上运行这两个线程 - 我们刚刚启动了这段代码 [两个线程开始,神奇地,几乎完全同时启动,不像在真实系统中,相隔数千个时钟周期],线程一个人将读取 的值x,更新它,然后将它写回。现在,线程 2 也在运行,它也会读取 的值x,更新它,然后将它写回。为此,它实际上需要询问其他处理器“x您的缓存中是否有(新值),如果有,请给我一份副本”。当然,处理器 1 将有一个新值,因为它刚刚存储了x. 现在,该缓存行是“共享的”(我们的两个线程都有该值的副本)。线程二更新值并将其写回内存。当它这样做时,该处理器发出另一个信号,说“如果有人持有 的值x,请删除它,因为我刚刚更新了该值”。
当然,完全有可能两个线程读取 的相同值x,更新为相同的新值,然后将其写回为相同的新修改值。迟早一个处理器会写回一个低于另一个处理器写入的值的值,因为它落后了一点......
栅栏操作将有助于确保写入内存的数据在下一个操作发生之前实际上已经完全缓存,因为正如您所说,在它们实际到达内存之前,有写缓冲区来保存内存更新。如果您没有栅栏指令,您的处理器可能会严重异相,并且在另一个有时间说“您有新值x吗?”之前不止一次更新该值。- 然而,它并不能真正帮助防止处理器 1 向处理器 2 请求数据,而处理器 2 立即“返回”请求它,从而以系统可以实现的速度来回快速地来回传送缓存内容。
为确保只有一个处理器更新某些共享值,您需要使用所谓的原子指令。这些特殊指令旨在与写入缓冲区和缓存一起操作,以确保只有一个处理器实际保存正在更新的缓存行的最新值,而没有其他处理器能够更新该处理器完成更新之前的值。所以你永远不会得到“读取相同的值x并写回相同的值x”或任何类似的东西。
由于缓存不适用于单个字节或单个整数大小的内容,因此您也可以使用“错误共享”。例如:
int x, y;
void thread1_code()
{
while(!done) x++;
}
void thread2_code()
{
while(!done) y++;
}
Run Code Online (Sandbox Code Playgroud)
现在,x并且y实际上不是相同的变量,但它们(很可能,但我们不能 100% 肯定地知道)位于相同的 16、32、64 或 128 字节的缓存行中(取决于处理器架构) . 因此,尽管x和y是不同的,但当一个处理器说“我刚刚更新x,请删除任何副本”时,另一个处理器将y在删除x. 我有这样一个例子,其中一些代码正在做:
struct {
int x[num_threads];
... lots more stuff in the same way
} global_var;
void thread_code()
{
...
global_var.x[my_thread_number]++;
...
}
Run Code Online (Sandbox Code Playgroud)
当然,两个线程会紧挨着更新值,性能很垃圾(比我们通过以下方式修复它时慢了大约 6 倍:
struct
{
int x;
... more stuff here ...
} global_var[num_threads];
void thread_code()
{
...
global_var[my_thread_number].x++;
...
}
Run Code Online (Sandbox Code Playgroud)
编辑以澄清:
fence不会(正如我最近的编辑所解释的那样)“帮助”防止在线程之间传输缓存内容。它本身也不会阻止数据在处理器之间不同步地更新——但是,它确实确保处理器执行fence操作不会继续执行其他内存操作,直到此特定操作的内存内容“离开”处理器内核本身。由于存在各种流水线阶段,并且大多数现代 CPU 具有多个执行单元,因此在执行流中,一个单元很可能“领先”于另一个在技术上“落后”的单元。围栏将确保“一切都在这里完成”。这有点像一级方程式赛车中那个拿着大刹车板的人,它确保车手在所有新轮胎都安全地装在车上之前不会离开轮胎更换(如果每个人都做他们应该做的)。
MESI 或 MOESI 协议是一种状态机系统,可确保不同处理器之间的操作正确完成。一个处理器可以有一个修改的值(在这种情况下,一个信号被发送到所有其他处理器以“停止使用旧值”),一个处理器可能“拥有”这个值(它是这个数据的持有者,并且可以修改价值),一个处理器可能有“独占”价值(它是价值的唯一持有者,其他人都摆脱了他们的副本),它可能是“共享的”(一个以上的处理器有一个副本,但这个处理器不应该更新值 - 它不是数据的“所有者”)或无效(数据不在缓存中)。MESI 没有“拥有”模式,这意味着在监听总线上有更多的流量(“监听”的意思是“xx
[1] 是的,处理器编号通常从零开始,但是在我写下这个附加段落时,我已经懒得回去将 thread1 重命名为 thread0,将 thread2 重命名为 thread1。
| 归档时间: |
|
| 查看次数: |
651 次 |
| 最近记录: |