Skip to main content

acquire release 实现内存一致性

背景

  1. 在单线程场景中,CPU 通常会保证程序顺序(Program Order) 的可见性,即单线程内的指令会按照代码编写的顺序执行(或看起来像是按顺序执行)存储器读写的结果也会符合单线程的预期
    1. 即使CPU有乱序功能,也会通过scoreboard等方式来处理data hazard,address hazard等,确保单线程内的内存访问都是保续的。即使现代的CPU都是超标量处理器。
  2. 但在多线程或多处理器(multi-hart)场景中,要实现多线程同时正确的对一个内存操作就会遇到问题
    1. 乱序执行(Out-of-Order Execution)
    2. 缓存分层(Cache Hierarchy),
    3. 存储缓冲区(Store Buffer)CPU有各级的存储器cache,in-flight指令暂存器
    4. 其他线程观察到的实际操作顺序与当前线程的程序顺序不一定一致
    5. 希望实现一个功能,利用一片多核都可以访问到的存储器来实现同步,这个存储器实现“锁”功能
      1. 处理器1,通过读“锁”,确定没人用,写“锁”,确保占用,不被别人使用
      2. 在处理器,读-写中间,处理器2可能就会写“锁”,从而造成错误
      3. 处理器1,准备通过写“锁”,来释放占用。但是写操作可能被乱序到当前线程的前面去执行,从而达不到保护的目的。
    6. 所以希望实现“原子指令”,实现获取“锁”,和释放“锁”的中间不会被其他的线程干扰,产生错误。
  3. 在多线程间建立 “同步关系”,通过限制 CPU 的重排序和缓存行为,确保
    1. 使用acquirereleasey语义,确保单线程内,和 LR.W 和 SC.W 指令之间是保序的。单线程内的指令实际执行顺序能被LR.W 和 SC.W控制,不收缓存和乱序的影响。
    2. 然后通过 LR.W 和 SC.W 指令,通过特殊的多线程都唯一的硬件,实现多线程之间的竞争。
  4. 从而确保
    1. 一个线程的写操作结果能被另一个线程的读操作正确感知
    2. 跨线程的操作顺序符合预期(避免 “先写后读” 变成 “先读后写” 的错乱)

LR.W(Load-Reserved)​

​指令格式​
​行为描述​
  1. ​加载值​​:从内存地址 rs1读取32位数据到寄存器 rd
  2. ​设置保留标记​​:
    • 在缓存子系统(如L1 Cache)中标记该地址为 ​​Reserved​​(保留状态)。
    • 硬件会记录当前核心(Hart)对该地址的独占访问权。
  3. ​隐式acquire语义​​(若未显式指定):
    • 后续内存操作不会被重排序到 LR.W之前(保证加载操作的可见性)。
​硬件实现细节​
  • ​缓存行状态​​:目标地址对应的缓存行会被置为 ​​Exclusive​​ 或 ​​Modified​​ 状态(取决于一致性协议,如MESI)。
  • ​保留标记的存储​​:
    • 通常由缓存控制器维护一个 ​​Reservation Register​​(保留寄存器),记录保留地址和核心ID。
    • 其他核心的写操作会清除该地址的保留标记(通过缓存一致性协议广播Invalidate)。

SC.W(Store-Conditional)​

​指令格式​
​行为描述​
  1. ​检查保留标记​​:
    • 若地址 rs1的保留标记仍被当前核心持有(未被其他核心修改):
      • 执行存储:将 rs2的值写入 rs1
      • 清除保留标记。
      • rd写入 0(成功)。
    • 若保留标记已失效(其他核心修改了地址 rs1):
      • 放弃存储。
      • rd写入 非0值(通常为1,失败)。
  2. ​隐式release语义​​(若未显式指定):
    • 之前的内存操作不会被重排序到 SC.W之后(保证存储操作的全局可见性)。
​硬件实现细节​
  • ​原子性保证​​:
    • 通过缓存一致性协议(如MESI)锁定缓存行,确保“检查-存储”的原子性。
    • 若其他核心发起写请求,缓存控制器会清除保留标记并响应Invalidate。
  • ​总线事务​​:
    • 成功时生成 ​​Atomic Write​​ 总线事务。
    • 失败时不发起存储请求。

典型用例:自旋锁实现​

# 加锁(使用aq/rl语义确保屏障)
lock:
  lr.w.aq t0, (a0)       # 带acquire的加载保留
  bnez    t0, lock       # 检查锁是否被占用(t0!=0则重试)
  li      t1, 1
  sc.w.rl t0, t1, (a0)   # 带release的条件存储
  bnez    t0, lock       # 若存储失败(t0!=0),重试

# 解锁
unlock:
  sw.rl   zero, (a0)     # 带release的存储,释放锁

多核交互场景示例​

Core 0                          Core 1
======                          ======
1. lr.w t0, (x)                3. lr.w t0, (x)
   - 加载x=0,设置保留标记          - 加载x=0,设置保留标记
2. sc.w t1, 1, (x)             4. sc.w t1, 1, (x)
   - 保留有效,存储成功(x=1)       - 保留已失效(因Core 0修改x),存储失败
   - t1=0                         - t1=1