Operating Systems-tep Chapter4和Chapter5 读书笔记

Chapter4 插曲:线程API

  • pthread_create 线程创建

  • pthread_join 等待线程执行完成,可以接收等待线程执行完后的返回值

  • 一个创建多个线程去并行执行特定任务并行程序会用join来确保所有的工作都完成,才退出或进入下一阶段工作。

  • phread_mutex_lock 是对一个锁变量上锁,pthread_mutex_unlock是对一个锁变量进行解锁。pthread_mutex_init 是初始化锁变量

  • phread_mutex_trylock 会在锁已经被持有时返回失败,phread_mutex_timedlock 会在得到了锁或者超时之后返回,因此,timelock函数在超时值为0时会退化成trylock函数。

  • pthread_cond_wait 会使调用线程休眠,等待其他线程唤醒它,wait函数会在线程休眠前释放这个锁。

  • 书中列举了一些tips:

在这里插入图片描述

Chapter5 锁

评估锁

评估锁的几个条件:

  1. 锁能够实现基本的任务,即提供互斥量。基本地,这个锁有效吗,能够阻止多线程进入同一个临界区吗?
  2. 公平。一旦锁空闲了,是否每个竞争该锁的线程都被公平对待?是否有竞争该锁的线程处于饥饿状态而一直得不到锁?
  3. 性能。一个是没有竞争的情况:当只有一个线程在运行、获取、释放锁,锁的开销有多少?另一个是多线程在单CPU上竞争同一个锁的情况,是否需要担心其性能?最后一点,当引入多CPU,各个CPU上的多个线程竞争同一个锁的性能如何?

中断控制

最早的互斥量的一个解决方案是为临界区关中断,这种方法是为单处理器系统设计的。其代码类似于下面:

在这里插入图片描述

假设程序运行在一个单处理器的系统上。通过在进入临界区前关中断(用某种特殊的硬件指令),我们可以确保进入临界区的代码不会被中断,因此就可以像原子一样执行。临界区代码执行结束后重新开中断(硬件指令),因此程序就可以像平常一样继续。

缺点:

  1. 用关中断作为一种通用的同步方案要求对应用程序太多信任。
  2. 这种方法在多处理器上行不通,如果多个线程运行在多个不同的CPU上,每个线程都试图进入相同的临界区。无论中断是否已经关了,线程还可以运行在其他的处理器上,并因此进入临界区。
  3. 效率很低,与普通指令的执行相比,在现代CPU上中断的开关通常会慢一些。

test-and-set(原子交换)

在这里插入图片描述

上面的代码中,我们用一个简单的flag变量来表示该锁是否已被持有。

当线程进入临界区的时候会调用lock(),lock()会测试flag是否等于1,不等于1会将flag置为1表示该线程已持有这把锁,当临界区代码执行完后,调用unlock()方法将flag置为0,表示该线程并不占用这把锁。

如果第一个线程在临界区里时,另一个线程恰好调用了lock(),这个线程就会在while循环里自选,等待持有锁的线程调用unlock()使flag=0,等待的线程就会跳出while循环,并将flag置为1,占有这把锁,然后进入临界区。

但是上述代码还是两个问题:1.正确性。2.性能

如果第一个线程进入临界区调用lock()方法,执行到while循环判定结束之后,在执行flag=1之前系统中断此线程,这时第二个线程进入临界区调用lock()方法,占有这把锁,执行完lock()方法之后,系统又中断线程,执行第一个线程,第一个线程执行flag=1,占有这把锁,这就出现问题了,两个线程占用了同一把锁。

关于性能问题:性能问题是线程等待获取已经被持有锁的方法:它不停的检查flag值,这称为自旋等待。自选等待为了等待其他线程释放锁而浪费时间。这个时间浪费在单处理器上格外高,等待线程等待的那个线程甚至无法运行。

构造一个可行的自旋锁

test-and-set指令的代码片段:

在这里插入图片描述
test-and-set指令返回ptr指向的旧值,同时ptr的值更新为new。

这一系列操作是以原子形式执行的。

通过利用test-and-set构造出的简单自旋锁:

在这里插入图片描述

第一种情况:一个线程调用了lock(),并且目前并没有线程持有这把锁,因此flag=0。当线程调用了TestAndSet(flag,1),就会返回flag的旧值0,并且使flag=1,这样这个线程就占有了这把锁。当线程执行完临界区代码,会设置flag=0释放锁。

第二种情况:假设已经有一个线程获得了锁(flag=1),这时候当前线程调用lock()并执行TestAndSet(flag,1),这时TestAndSet()返回旧值1,并设置flag=1,只要锁被其他线程持有,TestAndSet()就会不停返回1,这个线程就会自旋到锁被释放。当flag被某个线程置为0,当前线程就会再次掉欧阳那个TestAndSet(),此时TestAndSet()会返回0,同时将flag置为1,线程就会获得该锁并进入临界区。

评估自旋锁

正确性

自旋锁一次只运行一个线程进入临界区,因此自旋锁是正确的。

公平性

自旋锁并没有公平性保证。

性能

在单 CPU 上,性能很糟糕,在多CPU上,性能还可以。

compare-and-swap

在这里插入图片描述

compare-and-swap 的基本思想是见此ptr指定地址的值是否与expected相等,如果相等则更新ptr指向地址的值为new,不相等什么都不足,最后返回ptr指向地址之前的值。

fetch-and-add

代码如下:

在这里插入图片描述

能够原子的将某一地址的值+1。

下面是用fetch-and-add指令来构造一个排队锁。

在这里插入图片描述

当一个线程希望获得锁时,它先对ticket值做一次fetch-and-add操作,此时这个值就作为这个线程的turn。然后全局共享变量lock-turn用来决定到了哪个线程。当某个线程的myturn==lock->turn时,那就轮到这个线程到临界区了。unlock()将lock-<turn的值加1,这样下一个等待的线程(如果存在)就可以进入临界区了

这个方法保证了所有的线程能够执行

如此多的自旋

上面的这些方法都非常的低效。当运行临界区的线程中断时,其他等待的线程执行时只能不停的自旋,这样是十分浪费资源的。

简单方法:放手吧,孩子!

当线程将要自旋时,换作放弃CPU给其他线程。

在这里插入图片描述

这个方法中出现了一个操作系统原语yield(),可以在需要放弃CPU让其他线程执行时调用它。yield是将一个调用者从运行状态转化成就绪状态的简单系统调用,从而推进其他线程执行。因此,yield过程实质上是对自己取消调度。

尽管这个方法比前面讲的自旋方法好,但是这个方法的开销还是很大,上下文切换的开销还是很显著的,因此会有大量的浪费。

队列:休眠而非自旋

park(),使调用线程进入休眠,unpark(threadID) 唤醒threadID指定的某个线程,这两个操作可以联合起来构造一个锁,将试图获得非空闲的线程转换至休眠,当锁被释放后就唤醒这个线程。

在这里插入图片描述
上面的代码实现了一个线程休眠队列。lock_t中的flag表示现在是否有线程占用这把锁,当有线程占用这把锁的时候,会把当前线程加入到休眠队列中,并将guard的值置为0.这时会有另一个线程会进入if判定,因为flag=1,所以会被加入到等待队列中,guard设为0,然后进入休眠状态。

假设 某个线程正要执行 park(),假定它要休眠到锁不再被持有。此时切换至另一个线程(即持有锁的线程)会导致问题,比如,如果该线程释放了锁。第一个线程紧接着的 park() 就会永远休眠。这个问题有时被称作 wakeup/waiting race;要避免它,还需要一些其他帮助。

解决方法:

  1. 添加第三个系统调用:setpark()。通过调用这个操作,线程可以表明它将要进入park了。如果刚好发生了中断,且其他线程在真正调用park之前调用了unpark,那么接下来的park就会立即返回而不是休眠。lock()里面的代码修改很少:

在这里插入图片描述

  1. 将guard传入内核。这样的话内核就可以采取预防措施来原子地释放锁并dequeue运行线程。

不同操作系统,不同支持

Linux提供的futex与Solaris提供的接口类似,但是futex提供了更多的内核功能。特别的,每个futex可以与某个特定的物理内存位置关联;与之关联的内存位置是内核队列。线程可以调用futex来休眠或者唤醒。

futex_wait(address,expected) 调用address处的值域expected相等时,使调用线程休眠。如果不相等该系统会立即返回。futex_wake(address)调用唤醒等待队列中的某个线程。下面是它们在Linux中的使用方法:

在这里插入图片描述

两段(two-phase)锁

如果在第一阶段的自旋中没有获得锁,就进入第二阶段,此阶段调用者进入休眠直到锁变为空闲态后才被唤醒。前面的 Linux 的锁就是这种锁的一种 形式,但是他仅仅自旋一次;这种锁的一个原则就是在调用 futex 进入休眠前可以在循环中自旋固定次数。 两段锁还是混合(hybrid)方法的另一个实例,这种方法将两个优秀的思想 结合以实现一个更好的方法。当然,这种方法是否真的更好还取决于很多其他因素,包括硬件环境、线程数和其他负载细节。通常,构造一个适用于所有可能情 况的通用锁,是很具有挑战性的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值