原子性是如何由CPU实现的



我已经被告知/在线读取缓存一致性协议MESI/MESIF:

http://en.wikipedia.org/wiki/MESI_protocol

也强制原子性——例如锁。然而,这对我来说真的真的没有意义,原因如下:

1) MESI管理所有指令的缓存访问。如果MESI也强制原子性,我们如何得到竞争条件?当然,所有的指令都是原子的,我们永远不会得到竞争条件?

2)如果MESI保证原子性,那么LOCK前缀的意义是什么?

3)如果原子指令和其他x86指令使用相同的缓存一致性模型来实现,为什么人们说原子指令会带来开销?

一般来说,谁能解释一下CPU是如何在底层实现锁的?

LOCK前缀有一个目的,那就是获取一个地址上的锁,然后指示MESI在随后的所有其他处理器上刷新该缓存行,以便所有其他处理器(或硬件设备)对该地址的读写阻塞,直到锁被释放(这是在指令结束时)。

LOCK前缀很慢(几百个周期),因为它必须在持续时间内同步总线,总线速度和延迟远低于CPU速度。

LOCK指令的一般操作

1. validate
2. establish address lock on cache line
3. wait for all processors to flush (MESI kicks in here)
4. perform operation within cache line
5. flush cache line to RAM (which releases the lock)

免责声明:其中大部分来自Pentium F00F错误的文档(其中验证部分在建立锁之后错误地完成),因此可能已经过时。

正如@voo所说,你混淆了一致性和原子性。

缓存一致性涵盖了许多场景,但基本的例子是,当两个不同的代理(多核芯片上的内核,多套接字芯片上的处理器等)访问同一行时,它们可能都将其缓存在本地。MESI保证当其中一个写入新值时,所有其他旧副本首先失效,以防止旧值被使用。作为一个副产品,这实际上保证了对内存的单个读或写访问的原子性,在cacheline粒度上,这是x86(以及许多其他体系结构)上CPU章程的一部分。它的作用不止于此——它是内存排序和一致性的关键部分,保证CPU提供给你。

但是,它不提供任何更大规模的原子性,这对于处理线程安全和临界区等概念至关重要。您所指的锁定操作是一个读-修改-写流,它在默认情况下不能保证是原子的(至少在普通cpu上不能),因为它由2个不同的内存访问组成。在没有锁定的情况下,CPU可能会在中间接收到窥探,并且必须根据MESI协议做出响应。下面的场景是完全合法的,例如:

  core 0       |      core 1
---------------------------------
y = read [x]   |
increment y    |    store [x] <- z 
               |
store [x] <- y |

意味着内核0上的内存增量操作没有按预期工作。例如,如果[x]持有一个互斥锁,你可能会认为它是空闲的,你设法抓住了它,而core 1已经拿走了它。

在锁定的核0上进行读-修改-写操作(x86提供了许多可能的选项,锁定的add/inc,锁定的比较-exchange等),将会使其他核停滞,直到操作完成,因此它本质上增强了核间协议以允许拒绝窥探。

应该注意的是,一个简单的MESI协议,如果正确使用替代保证(如围栏),可以提供无锁的方法来执行原子操作。

我认为问题的关键是,虽然缓存涉及到普通的内存操作,但对于原子操作,需要做更多的


后来添加…

对于普通操作:

  • 当写入内存时,您的典型核心/cpu将保持写入队列,以便一旦写操作被调度,内核/cpu继续处理指令,而其他一些机制处理清空挂起的写队列——与根据需要缓存。在一些处理器上,不需要挂起写操作

  • 从内存中读取时,如果所需的值不是立即可用时,内核/cpu可以继续处理指令,而其他一些机制执行所需的读取操作——与

所有这些都是为了让核心/cpu继续运行,尽可能地从访问真实内存的可怕业务中解耦,通过缓存层,这是非常慢的

现在,对于原子操作,核心/cpu的状态必须与缓存/内存的状态同步。

因此,对于"释放"存储:(a)写队列中的所有内容必须完成,在(b)"释放"写本身完成之前,在(c)正常处理可以继续之前。因此,在原子写入完成之前,可能必须放弃缓存/内存异步写入的所有好处。类似地,对于"获取"加载,任何在"获取"读之后的读都必须延迟。

碰巧的是,x86非常"表现良好"。它不会对写入进行重新排序,因此"发布"存储不需要任何额外的工作来确保它排在任何早期存储之后。在读取端,它也不需要为"获取"做任何特别的事情。如果两个或更多的内核/cpu正在读写同一块内存,那么将会有更多的缓存线失效和重新加载,以及随之而来的开销。当执行"顺序一致"存储时,它必须后跟显式的mfence操作,该操作将使cpu/核心暂停,直到从写队列中刷新所有写操作。的确,"顺序一致"更容易理解……但是对于那些对共享数据的访问是由锁保护的代码,"acquire"/"release"就足够了。

对于原子的"读-修改-写"及其条件版本,与缓存/内存的交互甚至更强。执行操作的cpu/核心不仅必须与缓存/内存的状态同步,它还必须安排访问原子操作对象的其他cpu/核心暂停,直到它完成并被写入(提交到缓存/内存)。这将取决于当前是否与其他cpu/core存在实际争用

最新更新