当任务之间相互独立的时候,任务的并行执行是比较容易的。但往往任务之间需要相互协作,这种协作通常意味着某些任务写的结果是其他任务需要读取的值。这时执行读任务的一方要知道写任务什么时候完成了写操作,才能安全地读回数据。就是说,任务之间需要同步(synchronize),否则就有数据竞争(data race)的危险,导致读数据错误而引起程序运行结果的改变。
数据竞争:假如来自不同线程的两个访问请求访问同一个地址,它们连续出来,并且至少其中一个是写操作,那么这两个存储访问形成数据竞争
下面举一个记者的例子来方便大家理解 如果一个故事需要由8个记者共同写作,假设一个记者要写总结,他要阅读所有之前的章节。因此,他必须知道其他记者什么时候可以完成各自的章节,然后他再撰写总结,这样他就不用担心写好总结后其他记者再对各自章节进行修改。所以,他们就需要很好地同步各个章节撰写和阅读的过程,这样总结才能和前面章节中所写的内容相一致。
在计算机中,同步机制要依赖硬件提供的同步指令,这些指令可由用户调用。在本篇博客中重点探讨加锁(lock)和解锁(unlock)同步操作的实现。采用加锁和解锁可以直接创立一个仅允许单个处理器操作的区域,叫做互斥(mutual exclusion)区。更复杂的同步机制实现也与此类似。
在多处理器中实现同步需要一组硬件原语,提供对存储单元进行原子读和原子写的能力,使得在进行存储器原子读或原子写操作时任何其他操作都不得插入,如果没有这样的硬件原语,那么建立同步进制的代价将会变得很高,并且随着处理器数量的增加情况将更为恶化。
建立硬件原语有若干可选的方案,这些方案都可以实现原子读和原子写的功能,并能用某种方法表示这些操作是否为原子操作。通常,体系设计人员并不希望基本硬件原语被用户使用,而是希望这些原语被系统程序员用来建立同步库,建立同步库的过程通常很复杂且难度较大。
我们用原子交换原语(atomic exchange或atomic swap)来演示如何建立基本同步机制。这个原语是将寄存器中的一个值和存储器的一个值相互交换。
假定存储器中某个单元来表示一个锁变量:其数值为0时表示解锁,为1时表示加锁。一个处理器尝试对锁单元加锁,方法是用寄存器中的1与该锁单元的值进行交换。交换以后该锁单元的新值为1,返回值(锁单元的原值)如果是1,表明这个锁已被其他处理器占用;否则返回值为0,表示锁是自由的,尝试加锁成功。此时锁单元已被修改为1,以防止任何其他处理器再来占用。
例如,考虑有两个处理器同时尝试进行交换操作,它们的竞争关系就会被破坏。因为其中只能有一个处理器先执行交换操作,并且返回0,那么第二个处理器执行完交换操作的时候返回值就变成了1。
**用交换原语实现同步的关键是操作的原子性:交换操作是不可分割的,并且由硬件对两个同时执行的交换操作进行排序。**有可能两个处理器同时尝试置位同步变量,但这两个处理器被设置为认为它们同时成功设置了同步变量是不可能的。实现单个的原子存储器操作给处理器的设计者带来了若干挑战,因为这要求存储器的读写操作都是由单条不可被中断的指令完成。
一种可行的方法是采用指令对,其中第二条指令返回一个表明这对指令是否原子执行的标志值。假如处理器的操作都是在这对指令之前或之后执行,这对指令就是原子的。因此,当一个指令对是原子的,就没有哪个处理器能够改变这两个指令执行之间的数据值。
在MIPS处理器中这一指令对包括一条叫作链接取数(load linked)的特殊取数指令和一条叫作条件存数(store conditional)的特殊存数指令。我们顺序地使用这两条指令:如果链接取数指令所指定的锁单元的内容在相同地址的条件存数指令执行前已被改变,那么条件存在指数就执行失败。我们定义条件存数指令完成以下功能:保存寄存器的值,并且如果执行成功则寄存器的值修改为1,如果失败则修改为0。因为链接取数指令返回锁单元的原始值,条件存数指令执行成功的时候才返回1,下面的指令序列即实现了存储单元的原子交换。存储器单元的地址由$s1中的值取出.
(hint:这里需要做一个澄清,条件存数指令的寄存器$t0是一个临时存储器,如果发生了其他存储器的插入(在ll与sc指令之间),其会瞬间归0(也就是执行失败),从而根据下面的指令序列-原子交换指令又需要从头开始执行)
again:addi $t0 $zero,1 ;copy locked value
ll $t1,0($s1) ;load linked
sc $t0,0($s1) ;store conditional
beq $t0,$zero,again ;branch if store fails
add $s4,$zero,$t1 ;put load value in $s4
在指令序列的最后,寄存器$s4中的值和s1指向的锁单元的值发生了原子交换。此时寄存器s4中的值为0,存储器锁单元的值为1.
因为sc成功执行后,t0寄存器的值(当前为1)就会存储到s1对应的内存地址的值(也就是锁单元的值).而当前t1寄存器的值为0,最后的add执行后,s4的值变为0
可以通过它们来构造其他的诸如原子比较和交换(atomic compare and swap)或者原子取后加(atomic fetch-and-increment)等同步原语.这些同步原语可以被用在一些并行编程模型中。其实现需要在ll指令和sc指令之间插入更多的指令,但不需要太多。
因为在链接取数指令执行之后,任何试图修改锁单元值的操作或者任何异常都将导致条件存数指令执行失败,所以在选择ii和sc之间的指令时就要格外注意。特别需要注意的事。允许使用的并且不会造成问题的只有寄存器-寄存器指令,而处理器可能由于重复的页错误而导致始终无法完成sc指令,从而使处理器处于一种死锁的状态。另外,链接存数和条件存数之间的指令数一定要尽可能少,这样才可以减少不相关的时间或者竞争资源的处理器所引起条件存数指令执行失败的频率。