第8章 对同步的硬件支持 摘录

为了保证并行程序执行的正确性和高效性,构建一个共享存储多处理器系统的硬件必须要解决缓存一致性、存储一致性和同步原语的支持等问题。

被广泛使用的同步原语包括锁lock、栅栏barrier和点对点同步(signal和wait信号量)。举例来说,锁和栅栏被大量使用在DOALL并行性和具有链式数据结构的应用程序中,而signal/wait同步对流水线DOCROSS并行性来说至关重要。

如今一种很使用的方式是将最低级别的同步原语以原子指令的形式在硬件上实现,然后将其他所有高级别的同步原语在软件中实现。通常在可扩展性(同步延迟和带宽如何随着更大数量的线程而扩展)和无竞争延迟(线程不同时尝试同步所带来的延迟)之间会作出权衡。

对于大型系统来说,在原子指令之上实现的软件栅栏和锁可能不具有足够的可扩展。对于这些系统来说,硬件栅栏的实现很常见。

8.1 锁的实现

8.1.1 对锁实现性能的评估

性能评价标准,考虑锁的实现:

        1)无竞争获取锁延迟:当线程间没有竞争时,获取一个锁所花费的时间。
        2)通信量:总的通信量,是参与竞争锁的线程或者处理器数的函数。通信量可以分为三个子部分,即当一个锁空间时获取产生的通信量、当一个锁不空闲 时锁 获取产生的通信量:锁释放时产生的通信量。
        3)公平性:线程同其他线程相比被允许持有锁的程度。公平性的标准是在一个锁实现中,线程饥饿的情况是否存在,即一个线程不能长时间地持有锁,即使这个锁在这段时间内是空闲的。
         4)存储:需要的存储空间时线程数的函数。一些锁的实现要去一个恒定的存储空间,这个存储空间与共享锁的线程数无关;而另一些锁的实现要求的存储空间是随着共享锁的线程数而线性增长的。

8.1.2 对原子指令的要求

        软件机制并不能扩展,因为执行的静态指令的数量,已经为了查看线程是否能够获得锁而需要检查的变量的数量,都会随着线程数的增加而增加。相反,如果一个原子指令能够执行一系列加载、比较其他指令和存储指令,那么可以实现一个简单的锁

int is_turn;
int is_ready[n] = {0};    // n是处理器数目

void Lock(int porcess)
{
    int other = 1- process;
    is_ready[process] = TRUE;
    is_turn = process;
    while (is_turn == process && is_ready[other] == TRUE)
}

void Unlock(int process)
{
    is_ready[process] = FALSE;
}

   在当今的系统中,大部分的处理器支持将一个原子指令作为最低级的原语,同时基于它还可以构建其他同步原语。一个原子指令以一种不可分割的方式执行一系列的读、修改和写操作,这些操作在执行时不可能分割。

lock: ld    R1, &lockvar
      bnz   R1, lock
      st    &lockvar, #1
      ret

unlock: st    &lockvar, #0
        ret 

以上指令必须原子性地执行,“原子”这个词表达了两件事。首先,它意味着要么整个序列都被完整执行,要么其中任何一条指令都不执行。其次,它表达了在任何给定的时间内,只有一条原子指令(无论来自那个处理器)能够被执行。下面列出一些经常使用:

        test-and-set Rx,M: 读取存储在存储单元M的值,将这个值与一个常数进行比较,如果他们想匹配,那么将寄存器Rx中的到存储单元M中。

        fetch-and-op M: 读取存储在在存储单元M的值,对这个值执行操作(如自增、减值、加法、减法),然后将得到的新值存储到存储单元M中。在有些情况下,会指定额外的操作数。

        exchange Rx, M:自动交换在存储单元M中的值和寄存器Rx的值。

        compare-and-swap Rx, Ry, M:比较存储单元M中的值和寄存器Rx中的值,如果它们匹配,将寄存器Ry中的值写到存储单元M中,然后拷贝寄存器Rx中的值到寄存器Ry中。

除了以上指令之外,最通用的一个指令是比较并交换CAS:与test-adn-set指令相比较,它能执行一个比较,但是与之相比较的是一个寄存器中任意值,而不是一个常数;与一个exchange指令相比,它可以交换寄存器和内存中的值,但是需要附加的条件。

读者可能会提出两个问题:

        1)一个原子指令如何确保原子性?
        2)一个原子指令如何被用于同步控制结构?

一个原子指令本质上为程序提供了一个保障:指令所代表的一系列操作将会被完整地执行。

缓存一致性协议提供了原子性被保障的基础。当遇到一个原子指令时,这个缓存一致性协议知道需要保证其原子性。他首先会获得存储单元M的“独家所有权”(通过将其他包含M的缓存块中的拷贝都置为无效)。当获得独家所有权后,这个协议会确保只有只有一个处理器能够访问这个块,而如果其他处理器在此时想要访问的话就会经历缓存缺失,接下来原子指令就可以执行了。而如果其他处理器在此时想要访问的话就会经历缓存缺失,接下来原子指令就可以执行了。在原子指令持续期间,其他处理器不允许“偷走”这个块。

在一个基于总线的多处理器中,一个阻止块(在基于总线的多处理器上)被“偷走”的方法是锁上或者预约总线直到指令完成。

一个更加常用的解决办法(亦可用在非基于总线的多处理器系统中)不是阻止其他对总线的请求,而是使用执行原子指令的处理器的一致性控制器,来对块的其他所有请求延迟响应直到原子指令完成,或者否定确认请求,这样请求者会在未来重复请求。

8.1.3 TS锁

在获取锁的尝试中的第一条指令时test-and-set指令,它原子地执行下面几个步骤:首先从lockvar所在的存储单元中读取值到寄存器R1中(使用一个独占的读指令,如BusRfx或者BusUpgr),将寄存器R1中的值与0相比较。第二条指令时分支指令,当R1中的值非0的时候分支指令回到标签lock,这样锁的获取可以被重新尝试。如果R1中的值是0,就意味着当到达分支指令时,因为原子性,test-and-set指令已经成功了。锁的释放只需要将0赋给lockvar即可,而不需要原子指令,因为此时只有一个线程在临界区,所有只有一个线程能够对锁进行释放,在锁释放的时候不会产生冲突。

lock: t&s    R1, &lockvar
      bnz    R1, lock
      ret


unlock:    st, &lockvar,    #0
           ret

  评价一下test-and-set锁实现。因为在成功获得一个锁的时候只需要一条原子指令和一条分支指令,所以无竞争获取锁的延迟很低,但是通信量需求非常高。每个锁获取都试图使其他拷贝失效,而不管这个获取成功与否。比如

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值