这是受到在Go论坛上一篇帖子的启发想到的问题。问题的大意是:“如果处理器保证正确对齐的写入是原子的,那么为什么还需要数据竞态检测器?”
答案是,原子这个词在这里有两种用法。第一个是OP引用的,是大多数微处理器的属性,只要写入的地址自然对齐即可,例如,如果它是32位,则始终将其写入到一个地址中,占位四的倍数-那么没有东西会注意到占位一半的值。
为了解释这意味着什么,请考虑相反的情况,即未对齐的写入,其中将32位值写入到其底部两位不为零的地址。在这种情况下,处理器必须跨越边界分两部分写入。这被称为破损写入, 因为总线上的观察者可以看到此部分更新的值。1
这些话来自多处理器普及之前的时代。那时,读写中断的观察者很可能是ISA,VESA或PCI总线上的其他代理,例如磁盘控制器或视频卡。但是,我们现在生活在多核时代,因此我们需要谈论缓存和可见性。
自从开始计算以来,CPU的运行速度始终快于主内存。也就是说,计算机的性能与其存储器的性能密切相关。这称为处理器/内存间隙。为了弥合这种差距,处理器采用了将最近访问的内存存储在更靠近处理器的小型高速缓存中。2因为高速缓存也将缓冲写回主内存,而一个对齐地址的特性会变成原子遗骸, 此时该写操作发生已变得不那么确定性。3这是原子一词的第二个使用范围,该词由sync/atomic
程序包实现。
在现代多处理器系统中,对主内存的写操作将在命中主内存之前在多级缓存中进行缓冲。这样做是为了隐藏主内存的等待时间,但是这样做意味着使用主内存的处理器之间的通信现在不精确。从内存读取的值可能已经被一个处理器覆盖,但是新值尚未通过各种缓存成功同步。
要解决这种歧义,需要使用内存屏障。内存写屏障操作告诉处理器它必须等待,直到其管道中所有未完成的操作(特别是写操作)都已刷新到主内存中为止。此操作还会使缓存无效
由其他处理器持有时,迫使它们直接从内存中检索新值。 读取也是如此,使用内存读取屏障来告诉处理器停止任何未完成的内存写入同步。
就Go而言,读和写内存屏障操作由程序包sync/atomic
处理,特别是 分别由 atomic.Load
和atomic.Store
处理。
为了回答OP的问题:为了安全地在两个goroutine之间的通信通道使用内存中的值,除非使用该sync/atomic
程序包,否则数据竞态检测器会抱怨。
从历史上看,大多数处理器(但不是英特尔)都将未对齐的写入定为非法,如果尝试了未对齐的读取或写入,则会导致报错。通过消除将未对齐的负载和存储转换为存储器子系统的严格对齐要求,在晶体管价格昂贵的时候,这简化了处理器的设计。然而,今天,几乎所有的微处理器都已经发展为允许不对齐的访问,这是以性能和原子写入属性的损失为代价的。
第一台具有缓存的生产计算机是IBM System / 360 Model 85。
这过于简单。在硬件级别,需要将物理地址范围取消缓存以进行读取或遵循直写而不是回写语义。为了讨论同一虚拟地址空间中两个goroutine之间的内存可见性,可以安全地忽略这些细节。
nitpicker的注释:从技术上讲,缓存行是无效的
即使大多数处理器允许不对齐的读写,内存上的原子操作也要求地址自然对齐,因为处理器之间的通信由高速缓存处理,通常以64字节长的高速缓存行进行操作。因此,未对齐的读取或写入可能会跨越两个高速缓存行,这将不可能在各个处理器之间进行原子同步。
有疑问加站长微信联系(非本文作者)