缓存一致性与同元素写入:为何MESI在两种同缓存行场景下表现不同?
同缓存行场景下MESI的一致性边界解析
基于Intel Haswell x86_64平台的实测场景,很多开发者会疑惑:既然场景2和场景3里两个线程操作的是完全相同的缓存行,为什么MESI协议能保证前者的结果正确,却挡不住后者的竞态错误?
核心本质是:MESI是缓存行级别的内存内容一致性协议,它从来不对指令执行的原子性、多线程并发操作同一地址的流程做保障。
两个场景的操作边界差异
先把两个同缓存行的场景做明确切割:
- 场景2:线程1修改同缓存行内的
A[0],线程2修改同缓存行内的A[1],两个操作的目标内存地址完全不重叠 - 场景3:两个线程都修改同缓存行内的
A[0],操作的目标内存地址完全重叠
为什么MESI能保障场景2的结果正确
MESI的核心运行规则是:任意CPU核心要修改缓存行内的任意字节,必须先拿到该缓存行的独占所有权,同时把其他核心上存储的同缓存行副本置为无效状态;修改完成后,缓存行内未被改动的字节内容会被完整保留,不会因为所有权转移被篡改。
在场景2的运行过程中:
- 线程1给
A[0]累加的过程中,每次触发写动作都会先获取缓存行的独占权,过程中虽然会让另一个核心上的同缓存行失效、造成来回乒乓的性能损耗,但当核心2后续拿到缓存行写权限时,缓存行里已经被核心1改好的A[0]值会完整保留,不会被核心2的操作覆盖 - 同理,核心2修改
A[1]的动作,也不会篡改核心1已经写回的A[0]的值 - 因为两个线程修改的是完全独立的内存位置,不存在交叉覆写的问题,哪怕缓存行在两个核心之间来回传递所有权,最终两个变量各自的累加值都会准确达到10000。
为什么MESI挡不住场景3的结果错误
这里很容易有一个认知误区:觉得缓存行一致了,并发操作同个变量就不会错。实际上,C语言里的*elem = *elem + 1根本不是单条原子指令,编译后会拆成三步独立的操作:
- 把
*elem对应的数值从内存/本地缓存读到CPU通用寄存器 - 在寄存器内完成数值+1的计算
- 把寄存器里算好的新值写回
*elem对应的内存地址
MESI在这个流程里能做的,仅仅是保障第三步写回时,CPU拿到的是当前最新的缓存行内容,不会拿过期副本覆盖新数据——但它完全管不住“第一步读旧值”到“第三步写新值”的这段间隙里,其他核心插进来对同个地址做修改的动作。
举个直观的打断流程就能看懂错误来源: - 核心1把
A[0]的初始值0读到自己的寄存器,准备做+1计算 - 此时核心2抢先拿到
A[0]所在缓存行的写权限,连续完成10000次累加,把A[0]的值更新为10000并写回缓存 - 等核心1终于拿到缓存行的写权限时,它不会重新读取当前
A[0]的最新值,而是直接把自己寄存器里算好的1(也就是之前读到的0+1)写回A[0]的地址 - 这一步写回直接覆盖了核心2累加出来的10000次结果,最终两个线程的总累加值远小于20000,也就是典型的竞态错误。
内容的提问来源于stack exchange,提问作者AliYekta
相关产品推荐
相关产品推荐

