并行与指令
一、引言
在并行计算中,任务通常需要协作,涉及共享数据的读写操作。为避免数据竞争(data race)导致的错误,任务间必须进行同步(synchronize),确保写操作完成后读操作才能安全执行。本节重点探讨MIPS架构中用于同步的硬件支持,特别是加锁(lock)和解锁(unlock)机制,以及链接取数(load linked, LL)和条件存数(store conditional, SC)指令对的实现。
关键概念:
- 数据竞争:当两个来自不同线程的访存请求访问同一地址,连续发生,且至少一个是写操作时,称为数据竞争,可能导致读写错误。
- 同步:任务间协调机制,确保读操作在写操作完成后进行。
- 互斥(mutual exclusion):通过加锁和解锁创建仅允许单一处理器操作的区域,防止并发访问冲突。
示例:8个记者协作撰写故事,其中总结者需等待其他记者完成章节撰写,需通过同步确保内容一致性。
二、同步的硬件支持
多处理器系统中,同步依赖硬件提供的原子操作原语,确保读写操作不可中断。原子操作要求在执行期间无其他操作插入,否则同步机制成本高昂,且随处理器数量增加而恶化。
1. 原子操作原语
体系结构设计者提供硬件原语(如原子交换),供系统程序员构建同步库,而非直接供用户使用。常见的原语包括:
- 原子交换(atomic exchange/swap):交换寄存器和存储器中的值,保证操作不可分割。
- 原子比较和交换(atomic compare and swap)。
- 原子取后加(atomic fetch-and-increment)。
原子性保证:
- 硬件对并发操作排序,确保只有一个处理器成功执行原子操作。
- 原子操作失败时,需重试,直到成功。
2. 锁机制
锁变量用于控制共享资源访问:
- 锁状态:
- 0:解锁(自由)。
- 1:加锁(被占用)。
- 加锁过程:
- 处理器用寄存器中的1与锁单元值交换。
- 返回锁单元原始值:
- 0:加锁成功,锁单元置为1。
- 1:锁已被占用,加锁失败,需重试。
- 原子性:交换操作不可分割,防止多个处理器同时认为加锁成功。
示例:两个处理器竞争加锁:
- 第一个处理器交换成功,返回0,锁置为1。
- 第二个处理器交换返回1,失败,需重试。
三、MIPS中的同步实现
MIPS使用链接取数(LL)和条件存数(SC)指令对实现原子操作。
1. 指令说明
- 链接取数(LL):
- 格式:
ll $rt, offset($rs)
- 功能:从地址
$rs+offset
读取值到$rt
,标记该地址供后续SC指令检查。
- 格式:
- 条件存数(SC):
- 格式:
sc $rt, offset($rs)
- 功能:尝试将
$rt
的值写入地址$rs+offset
,若成功,$rt
置为1;若失败(地址值被修改),$rt
置为0。
- 格式:
原子性保证:
- 若在LL和SC之间,锁单元值被其他处理器修改,SC失败,
$rt
置为0。 - 失败时,需重试整个指令序列。
2. 原子交换实现
示例:实现锁单元(地址在$s1
)与寄存器值的原子交换:
again:
addi $t0, $zero, 1 # $t0 = 1(准备写入锁)
ll $t1, 0($s1) # 读取锁值到$t1
sc $t0, 0($s1) # 尝试将$t0写入锁,若成功$t0=1,失败$t0=0
beq $t0, $zero, again # 若失败,跳转重试
add $s4, $zero, $t1 # 保存原始锁值到$s4
工作原理:
- LL读取锁值,SC尝试写入1。
- 若LL到SC间锁值未被修改,SC成功,锁置为1,
$t0=1
。 - 若锁值被修改,SC失败,
$t0=0
,跳转again
重试。 - 最终,
$s4
保存锁原始值,锁单元值为1。
3. 注意事项
- 指令选择:
- LL和SC间只能使用寄存器-寄存器指令,避免触发异常或修改锁值。
- 指令数需尽量少,减少竞争或无关事件导致SC失败的概率。
- 死锁风险:
- 重复的页面错误可能导致SC始终失败,需谨慎设计。
- 单处理器适用性:
- 在单处理器中,LL/SC对上下文切换敏感,切换会导致SC失败,确保原子性。
四、同步的应用场景
- 链接取数(LL)和条件存数(SC)原语适用于:
- 并行程序中的线程同步:协作线程需同步以正确读写共享数据。
- 单处理器上的进程同步:协作进程需同步以确保共享数据访问正确。
场景示例:
- 多线程程序:数据库事务中,多个线程更新同一记录,需加锁防止数据竞争。
- 操作系统:多进程共享内核数据结构,需同步避免竞争。
- 并行算法:如并行矩阵计算,线程间共享部分结果,需同步确保数据一致性。
五、扩展与总结
1. 其他同步原语
LL/SC机制可扩展为其他原语:
-
原子比较和交换:
- 比较锁值与预期值,相等则交换。
-
示例代码:
assembly ll $t1, 0($s1) # 读取锁值 bne $t1, $t2, fail # 若$t1 != 预期值,跳转失败 addi $t0, $zero, 1 # 准备写入新值 sc $t0, 0($s1) # 尝试写入 beq $t0, $zero, again # 失败重试
-
原子取后加:
- 读取值,递增后写回。
-
示例代码:
assembly ll $t1, 0($s1) # 读取值 addi $t2, $t1, 1 # 递增 sc $t2, 0($s1) # 尝试写回 beq $t2, $zero, again # 失败重试
2. 性能优化
- 减少重试:优化LL/SC间指令,降低竞争导致的SC失败率。
- 锁粒度:细化锁范围,减少互斥区大小,提升并发性能。
- 无锁同步:使用原子操作替代锁,适用于简单场景,如计数器递增。
3. 硬件设计挑战
- 原子性实现:需确保LL/SC指令对不可中断,可能通过缓存一致性协议或总线锁定实现。
- 扩展性:多处理器系统中,同步开销随处理器数量增加,需高效硬件支持(如目录协议)。
- 单指令原子性:某些架构(如x86)提供单条原子指令(如
cmpxchg
),简化同步实现。
4. 总结
- 同步重要性:避免数据竞争,确保并行程序正确性。
- MIPS LL/SC:通过原子指令对实现锁机制,支持多处理器和单处理器同步。
- 设计原则:
- 原子性:操作不可分割。
- 最小化竞争:减少LL/SC间指令和锁冲突。
- 扩展性:支持复杂同步原语和多线程场景。
- 应用前景:随着多核处理器普及,高效同步机制对并行计算性能至关重要。