《Tsinghua os mooc》第17~20讲 同步互斥、信号量、管程、死锁
第十七讲 同步互斥
- 进程并发执行 - 好处1:共享资源。比如:多个用户使用同一台计算机。
- 好处2:加速。I/O操作和CPU计算可以重叠(并行)。
- 好处3:模块化。
- 将大程序分解成小程序。以编译为例,gcc会调用cpp,cc1,cc2,as,ld。
- 使系统易于复用和扩展。程序可划分成多个模块放在多个处理器上并行执行。
 
 
- 原子操作 - 原子操作是指一次不存在任何中断或失败的操作。要么操作成功完成,或者操作没有执行,不会出现部分执行的状态。
- 操作系统需要利用同步机制在并发执行的同时,保证一些操作是原子操作。
 
- 由于不是原子操作而带来错误的一个例子:并发创建新进程时的标识分配。如下面,标识分配用C语言表达是一个语句,翻译成机器指令后是4条机器指令。假设next_pid开始是100,有两个进程A和B,如果进程A执行完前2条机器指令后,CPU切换到进程B执行完4条机器指令,再切回A执行完后2条指令。那么,进程A和B分配到的new_pid都是101,而next_pid最后也被更新为101,显然出现了Bug。 
// C code
new_pid = next_pid++
// Machine Code
LOAD next_pid Reg1
STORE Reg1 new_pid
INC Reg1
STORE Reg1 next_pid
- 利用两个原子操作实现一个锁(lock)
- Lock.Acquire():在锁被释放前一直等待,然后获得锁;如果两个线程都在等待同一个锁,并且同时发现锁被释放了,那么只有一个能够获得锁。
- Lock.Release():解锁并唤醒任何等待中的进程。
 
breadlock.Acquire();  // 进入临界区
if (nobread) {        // 临
  buy bread;          // 界
 }                    // 区
breadlock.Release();  // 退出临界区
- 进程的交互关系:相互感知程度 - 相互不感知(完全不了解其它进程的存在):进程之间相互独立,一个进程的操作对其他进程的结果无影响
- 间接感知(双方都与第三方交互,如共享资源):进程之间通过共享来协作,一个进程的结果依赖于共享资源的状态
- 直接感知(双方直接交互,如通信):进程之间通过通信来协作,一个进程的结果依赖于从其他进程获得的信息
 
- 进程的交互关系 - 互斥 ( mutual exclusion ) :一个进程占用资源,其它进程不能使用
- 死锁(deadlock):多个进程各占用部分资源,形成循环等待
- 饥饿(starvation):其他进程可能轮流占用资源,一个进程一直得不到资源
 
- 临界区(Critical Section) - 临界区(critical section):进程中访问临界资源的一段需要互斥执行的代码
- 进入区(entry section):检查可否进入临界区的一段代码。如可进入,设置相应"正在访问临界区"标志
- 退出区(exit section):清除“正在访问临界区”标志
- 剩余区(remainder section):代码中的其余部分
 
entry section
   critical section
exit section
   remainder section
- 临界区的访问规则 - 空闲则入:没有进程在临界区时,任何进程可进入
- 忙则等待:有进程在临界区时,其他进程均不能进入临界区
- 有限等待:等待进入临界区的进程不能无限期等待
- 让权等待(可选):不能进入临界区的进程,应释放CPU(如转换到阻塞状态)
 
- 临界区的实现方法 - 禁用中断(仅适用于单处理器)
- 软件方法(复杂)
- 更高级的抽象方法(单处理器或多处理器均可)
 
- 临界区的硬件实现方法:禁用硬件中断 - 没有中断,没有上下文切换,因此没有并发。硬件将中断处理延迟到中断被启用之后,现代计算机体系结构都提供指令来实现禁用中断
- 进入临界区:禁止所有中断,并保存标志
- 离开临界区:使能所有中断,并恢复标志
- 缺点:禁用中断后,进程无法被停止,整个系统都会为此停下来,可能导致其他进程处于饥饿状态;临界区可能很长,无法确定响应中断所需的时间(可能存在硬件影响)
- 要小心地用
 
local_irq_save(unsigned long flags);
critical section
local_irq_restore(unsigned long flags);
- 临界区的软件实现方法之一:Peterson算法
- 共享变量
 int turn; // 表示该谁进入临界区
 boolean flag[]; // 表示进程是否准备好进入临界区
 - 代码实现
 
do {
    flag[i] = true;
    turn = j;
    while ( flag[j] && turn == j);
        CRITICAL SECTION
    flag[i] = false;
        REMAINDER SECTION
   } while (true);
- 临界区的软件实现方法之二(支持多个进程):Dekkers算法
flag[0]:= false; flag[1]:= false; turn:= 0;//or1
do {
    flag[i] = true;
    while flag[j] == true {
        if turn ≠ i {
            flag[i] := false
            while turn ≠ i { }
            flag[i] := true
        }
    }
    CRITICAL SECTION
    turn := j
    flag[i] = false;
    EMAINDER SECTION
   } while (true);
- 临界区的软件实现方法之三:N线程的软件方法(Eisenberg和McGuire) - 线程Ti要等待从turn到i-1的线程都退出临界区后访问临界区
- 线程Ti退出时,把turn改成下一个请求线程
 
- 基于软件的解决方法的分析 - 复杂:需要两个进程间的共享数据项
- 需要忙等待:浪费CPU时间
 
- 临界区的更高级的抽象实现方法:操作系统提供更高级的编程抽象来简化进程同步,例如锁、信号量,而它们是基于硬件提供的同步原语来构建的,比如中断禁用、原子操作指令等。 
- 锁是一个抽象的数据结构 - 一个二进制变量(锁定/解锁)
- Lock::Acquire():原子操作。锁被释放前一直等待,然后得到锁
- Lock::Release():原子操作。释放锁,唤醒任何等待的进程
- 使用锁来控制临界区访问
 
lock_next_pid->Acquire();
new_pid = next_pid++ ;
lock_next_pid->Release();
- 原子操作指令 - 现代CPU体系结构都提供一些特殊的原子操作指令
- 测试和置位(Test-and-Set )指令:从内存单元中读取值,测试该值是否为1(然后返回真或假),将内存单元值设置为1
 - boolean TestAndSet (boolean *target)
 {
 boolean rv = *target;
 *target = true;
 return rv:
 }
 - 交换指令(exchange):交换内存中的两个值
 - void Exchange (boolean *a, boolean *b)
 {
 boolean temp = *a;
 *a = *b;
 *b = temp:
 }
 
- 使用TS指令实现自旋锁(spinlock) - 如果锁被释放,那么TS指令读取0并将值设置为1,锁被设置为忙并且需要等待完成
- 忙则等待。如果锁处于忙状态,那么TS指令读取1并将值设置为1,不改变锁的状态并且需要循环
- 线程在等待的时候消耗CPU时间
 - class Lock {
 int value = 0;
 } Lock::Acquire() {
 while (test-and-set(value))
 ; //spin
 } Lock::Release() {
 value = 0;
 }
 
- 无忙等待锁 - class Lock {
 int value = 0;
 WaitQueue q;
 } Lock::Acquire() {
 while (test-and-set(value)) {
 add this TCB to wait queue q;
 schedule();
 }
 } Lock::Release() {
 value = 0;
 remove one thread t from q;
 wakeup(t);
 }
 
- 原子操作指令锁的特征 - 优点
- 适用于单处理器或者共享主存的多处理器中任意数量的进程同步
- 简单并且容易证明
- 支持多临界区
 
- 缺点
- 忙等待消耗处理器时间
- 可能导致饥饿、进程离开临界区时有多个等待进程的情况
- 死锁:拥有临界区的低优先级进程请求访问临界区,而高优先级进程获得处理器并等待临界区
 
 
- 优点
第十八讲 信号量与管程
- 自旋锁、互斥锁、条件变量、信号量 - 自旋锁:一直尝试加锁,只要没有锁上,就不断尝试。
- 互斥锁:尝试加锁,如果没有锁上,则让出CPU给其他进程使用,等到锁的状态发生变化时再唤醒该进程。涉及到上下文切换,因此操作开销比自旋锁大。
- 条件变量:与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。条件变量是在多线程程序中用来实现“等待->唤醒”逻辑常用的方法。互斥量在允许或堵塞对临界区的访问上是很有用的,条件变量则允许线程由于一些未达到的条件而堵塞。通常条件变量和互斥锁同时使用,这种模式用于让一个线程锁住一个互斥量,然后当它不能获得它期待的结果时等待一个条件变量。最后另一个线程会向它发信号,使它可以继续执行。
- 信号量:是一种更高级的同步机制,mutex可以说是semaphore在仅取值0/1时的特例。Semaphore可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。信号量的主要用途是调度线程,具体而言就是:一些线程生产(increase)同时另一些线程消费(decrease),semaphore可以让生产和消费保持合乎逻辑的执行顺序。
- 互斥锁,同步锁,临界区,互斥量,信号量,自旋锁之间联系是什么? - Tim Chen的回答 - 知乎
- 如何理解互斥锁、条件锁、读写锁以及自旋锁? - 邱昊宇的回答 - 知乎
- semaphore和mutex的区别? - 二律背反的回答 - 知乎
 
- 信号量 vs 软件同步 - 信号量是操作系统提供的一种协调共享资源访问的方法。OS是管理者,地位高于进程。
- 软件同步是平等线程间的一种同步协商机制。
 
- 信号量 - 早期的操作系统的主要同步机制,现在已很少使用。
- 信号量是一种抽象数据类型,由一个整形 (sem)变量和两个原子操作组成。
- P():Prolaag (荷兰语尝试减少),sem减1,如sem<0, 进入等待, 否则继续
- V():(Verhoog (荷兰语增加)),sem加1
- 信号量是被保护的整数变量,初始化完成后,只能通过P()和V()操作修改,由操作系统保证,P/V操作是原子操作
- P() 可能阻塞,V()不会阻塞
- 通常假定信号量是“公平的”,线程不会被无限期阻塞在P()操作,假定信号量等待按先进先出排队
 
- 信号量的实现 
classSemaphore {
    int sem;
    WaitQueue q;
}
Semaphore::P() {
    sem--;
    if (sem < 0) {
        Add this thread t to q;
        block(p);
    }
}
Semaphore::V() {
    sem++;
    if (sem<=0) {
        Remove a thread t from q;
        wakeup(t);
    }
}
- 信号量分类 - 二进制信号量:资源数目为0或1
- 资源信号量:资源数目为任何非负值
- 两者等价,基于一个可以实现另一个
 
- 信号量的使用 - 互斥访问:临界区的互斥访问控制
- 每个临界区设置一个信号量,其初值为1
- 必须成对使用P()操作和V()操作,P()操作保证互斥访问临界资源,V()操作在使用后释放临界资源,P/V操作不能次序错误、重复或遗漏
 
- 条件同步:线程间的事件等待
- 每个条件同步设置一个信号量,其初值为0
 
 
- 互斥访问:临界区的互斥访问控制
- 生产者-消费者问题 - 一个或多个生产者在生成数据后放在一个缓冲区里
- 单个消费者从缓冲区取出数据处理
- 任何时刻只能有一个生产者或消费者可访问缓冲区
- 可能存在竞争条件:假设只使用一个全局变量count来记录缓冲区的数据项数,再假设消费者刚将count的值读到寄存器时,CPU切换到生产者进程,生产者向缓冲区加入一个数据,count加1,然后唤醒消费者。然而消费者此时在逻辑上并未睡眠,所以wakeup信号丢失。当消费者下次运行时,它测试寄存器的值,发现count=0,于是睡眠。生产者迟早会填满整个缓冲区,然后睡眠。这样一来,两个进程都将永远睡眠下去。
 
- 用信号量解决生产者-消费者问题 
Class BoundedBuffer {
    mutex = new Semaphore(1);  // 二进制信号量
    fullBuffers = new Semaphore(0);   // 资源信号量
    emptyBuffers = new Semaphore(n);  // 资源信号量
}
BoundedBuffer::Deposit(c) {
    emptyBuffers->P();
    mutex->P();
    Add c to the buffer;
    mutex->V();
    fullBuffers->V();
}
BoundedBuffer::Remove(c) {
    fullBuffers->P();
    mutex->P();
    Remove c from buffer;
    mutex->V();
    emptyBuffers->V();
}
- 使用信号量的困难 - 读/开发代码比较困难,程序员需要能运用信号量机制
- 容易出错,如使用的信号量已经被另一个线程占用、忘记释放信号量等
- 不能够处理死锁问题
 
- 管程(monitor) - 采用面向对象方法,简化了线程间的同步控制。伍注:信号量在实现同步时需要P/V多个变量,而且P/V操作有严格顺序,否则可能出现死锁;而管程只需要调用一个封装的函数即可。
- 任一时刻最多只有一个线程执行管程代码,这一特性使管程能有效地完成互斥。
- 正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复。这需要引入条件变量以及相关的两个操作:wait和signal。当一个管程过程发现它无法继续运行时,它会在某个条件变量上执行wait操作。该操作导致调用进程自身堵塞,并且还将另一个以前等在管程之外的进程调入管程。另一个管程过程可以通过对其伙伴正在等待的一个条件变量执行signal,来唤醒正在睡眠的伙伴进程。
 
- 条件变量是管程内的等待机制。进入管程的线程因资源被占用而进入等待状态,每个条件变量表示一种等待原因,对应一个等待队列。 
Class Condition {
    int numWaiting = 0;
    WaitQueue q;
}
Condition::Wait(lock){
    numWaiting++;
    Add this thread t  to q;
    release(lock);
    schedule(); //need mutex
    require(lock);
}
Condition::Signal(){
    if (numWaiting > 0) {
        Remove a thread t from q;
        wakeup(t); //need mutex
        numWaiting--;
    }
}
- 用管程实现生产者-消费者问题
classBoundedBuffer {
    …
    Lock lock;
    int count = 0;
    Condition notFull, notEmpty;
}
BoundedBuffer::Deposit(c) {
    lock->Acquire();
    while (count == n)
        notFull.Wait(&lock);
    Add c to the buffer;
    count++;
    notEmpty.Signal();
    lock->Release();
}
BoundedBuffer::Remove(c) {
    lock->Acquire();
    while (count == 0)
      notEmpty.Wait(&lock);
    Remove c from buffer;
    count--;
    notFull.Signal();
    lock->Release();
}
- 管程条件变量的释放处理方式 - Hansen管程:让新唤醒的进程运行,而挂起通知进程。少切换、高效。主要用于真实OS和Java中。
- Hoare管程:执行signal的进程必须立即退出进程。多切换、低效。概念上更简单,主要见于教材中。
 
- 哲学家问题的一个解法(没有死锁,可供多个哲学家就餐) 
#define   N   5                     // 哲学家个数
semaphore fork[5];                  // 信号量初值为1
void philosopher(int   i)         // 哲学家编号:0 - 4
{
    while(TRUE)
    {
        think( );                   // 哲学家在思考
        if (i%2 == 0) {
            P(fork[i]);	            // 去拿左边的叉子
            P(fork[(i + 1) % N]);   // 去拿右边的叉子
        } else {
            P(fork[(i + 1) % N]);   // 去拿右边的叉子
            P(fork[i]);             // 去拿左边的叉子
        }
        eat( );                     // 吃面条中….
        V(fork[i]);		            // 放下左边的叉子
        V(fork[(i + 1) % N]);	    // 放下右边的叉子
    }
}
- 读者-写者问题 
- 基本同步方法 
  
第十九讲 实验七 同步互斥
- 哲学家就餐问题的底层支撑技术
  
第二十讲 死锁与进程通信
- 目前大多数操作系统不负责死锁处理,因其开销较大。
《Tsinghua os mooc》第17~20讲 同步互斥、信号量、管程、死锁的更多相关文章
- 《Tsinghua os mooc》第1~4讲 启动、中断、异常和系统调用
		资源 OS2018Spring课程资料首页 uCore OS在线实验指导书 ucore实验基准源代码 MOOC OS习题集 OS课堂练习 Piazza问答平台 暂时无法注册 疑问 为什么用户态和内核态 ... 
- 《Tsinghua os mooc》第15~16讲 处理机调度
		第十五讲 处理机调度 进程调度时机 非抢占系统中,当前进程主动放弃CPU时发生调度,分为两种情况: 进程从运行状态切换到等待状态 进程被终结了 可抢占系统中,中断请求被服务例程响应完成时发生调度,也分 ... 
- 《Tsinghua os mooc》第21~22讲 文件系统
		第二十一讲 文件系统 文件系统是操作系统中管理持久性数据的子系统,提供数据存储和访问功能. 组织.检索.读写访问数据 大多数计算机系统都有文件系统 Google 也是一个文件系统 文件是具有符号名,由 ... 
- 《Tsinghua os mooc》第11~14讲 进程和线程
		第十一讲 进程和线程 进程 vs 程序 程序 = 文件 (静态的可执行文件) 进程 = 执行中的程序 = 程序 + 执行状态 进程的组成包括程序.数据和进程控制块 同一个程序的多次执行过程对应为不同进 ... 
- 《马上有招儿:PPT商务演示精选20讲(全彩) 》
		<马上有招儿:PPT商务演示精选20讲(全彩) > 基本信息 作者:马建强 霍然 出版社:电子工业出版社 ISBN:9787121225123 上架时间:2014-3-11 出版日期 ... 
- PPT2010学习笔记(共20讲)
		第1讲 商务PPT中的必备元素 # 设计需打破规范 第2讲 封面页设计(一) 大图型封面页 # 基础知识点: 插入矩形和圆形 设置半透明色 设置字体变形效果 图片增强工具 利用过渡色虚化图片边缘 ... 
- 基数排序的可复用实现(C++11/14/17/20)
		基数排序,是对整数类型的一种排序方法,有MSD (most significant digit)和LSD (least significant digit)两种.MSD将每个数按照高位分为若干个桶(按 ... 
- js如何判断一组数字是否连续,得到一个临时数组[[3,4],[13,14,15],[17],[20],[22]];
		var arrange = function(arr){ var result = [], temp = []; arr.sort(function(source, dest){ return sou ... 
- iOS  _BSMachError: (os/kern) invalid capability (20)
		_BSMachError: (os/kern) invalid capability (20) 解决办法:将info.plist里面的en改为United States 2016-04-18 22:4 ... 
随机推荐
- [转]CentOS 7安装Python3.6过程(让linux系统共存Python2和Python3环境)
			CentOS 7系统自带了python2,不过可以不用2版本,直接使用python3运行python脚本就可以,但是千万别去动系统自带的python2,因为有程序依赖目前的python2环境,比如yu ... 
- Kmeans聚类(lena图)
			lena512.raw 下载地址:https://files.cnblogs.com/files/jzcbest1016/lena512_20171219131444306.rar .raw文件可以用 ... 
- Redis使用总结(二、缓存和数据库双写一致性问题)
			首先,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用.在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作. 但是在更新缓存方面,对于更新完数据库,是更新缓存呢,还是删除缓存.又或者 ... 
- Hadoop(3)如何构建HDFS--HA,YARN---HA
			什么是HA? HA的意思是High Availability高可用,指当当前工作中的机器宕机后,会自动处理这个异常,并将工作无缝地转移到其他备用机器上去,以来保证服务的高可用. HA方式安装部署才是最 ... 
- Java核心复习——J.U.C AbstractQueuedSynchronizer
			第一眼看到AbstractQueuedSynchronizer,通常都会有这几个问题. AbstractQueuedSynchronizer为什么要搞这么一个类? 这个类是干什么的.有什么用? 这个类 ... 
- thymeleaf  与shiro  整合错误
			错误 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // ... 
- 移动端安卓和 IOS 开发框架 Framework7 布局
			对应的各种效果,Framework7 里面实现的方式比较多,这里我就只写我用的一种,样式有的自己修改了的,想看官方详细的参见 http://framework7.cn 一.手风琴布局Accordion ... 
- android: Context引起的内存泄露问题
			错误的使用Context可能会导致内存泄漏,典型的例子就是单例模式时引用不合适的Context. public class SingleInstance { private static Single ... 
- 安卓打包apk
			打apk包的环境依赖 1.jdk 2.sdk 3.ndk 打apk包的工具 gradle mkdir /usr/local/Android cd /usr/local/Android mkdir sd ... 
- Centos7.4服务器安装Laravel5.7详细讲解(2018-10-27)
			一.在阿里云或者腾讯云选择Centos7并购买服务器 二.安装宝塔面板和php运行环境 1.输入命令 yum install -y wget && wget -O install.sh ... 
