zoukankan      html  css  js  c++  java
  • 清华大学操作系统公开课(八)-同步互斥

    1.什么是互斥

    在计算机执行过程中,对于多个任务,它们共享着一个资源,要求对该资源的存取过程是排他的。

    2.为什么要有互斥

    不考虑SMP情况,仅分析单CPU情况,因为SMP只不过是更复杂的一种情况,原理类似。
    有如下代码片段,其中share_data是一个全局变量。

    int share_data = 0;
    
    void foo(void)
    {
    	share_data++;		//写共享资源
    	show share_data;	//读共享资源
    }
    

    2.1线程间

    如果两个线程都执行上述片段,执行过程如下图所示:

    首先是Thread A开始执行,此时share_data为0,执行完①后,如果执行流程不被打断,则继续往下走,最终show share_data得到的数据应该是1。但是当Thread A执行完步骤①后,CPU发生了调度,切到Thread B上执行②,而share_data对于两个线程是共享的,所以Thread B此刻看到的share_data是经过了一次自增的,为1。最终执行完流程③,④。Thread A看到的结果是share_data自增了两次,与预期不符。

    2.2线程与中断间

    还一种情况是代码片段分别在一个线程和中断中:

    过程和上面类似,只不过执行流程是被中断打断的。

    3.并发和竞态

    以上多个执行流在一个时间段并发(Concurrency)执行,并且围绕共享资源进行访问导致了竞态(Race Conditions)。就是一种你争我夺的状态。而这个共享的资源就是他们争夺的对象,可以是(全局的变量,同一个硬件资源,文件系统上的一个文件等)。

    与并发常常一起提到的另一个词叫并行,并行指同一个时刻,同时进行,例如SMP上多个CPU执行多个线程这种情况。

    3.1如何解决竞态

    以上看到了竞态带来了我们预期之外的效果,按照我们程序的设计,对于一个功能模块,同样的输入应该得到一致的输出才行。考虑生活中一个这样的问题:A和B住一起,冰箱里面没面包了就要去买,但是我们要避免两个人重复买面包这种情况。如何解决?

    • 方案一
      打开冰箱,发现没面包了,留张纸条贴在冰箱上,买回面包放进去,撕去纸条。用代码表示:
    do
    {
    	if (noNote)
    	{
    		if (noBread)
    		{
    			leave Note;
    			buy bread;
    			remove Note;
    		}
    	}
    }while (1);
    

    能解决问题吗?不能。假设A看到 if (noNote) ---> if (noBread),刚好有别的事打断了他,A出去了,这时B进来,也看到了if (noNote) ---> if (noBread),然后恰巧B也被打断了,A此时进来,于是他继续前面被打断的工作,留下纸条,买回面包,撕去纸条,走了。然后B回来了,继续前面被打断的工作,留下纸条,买回面包,撕去纸条,走了。这时会发现A和B都买了面包。

    • 方案二
      以上问题看上去好像是没提前贴好纸条导致,那如果先贴纸条呢
    do{
    	leave Note;
    	if (noNote)
    	{
    		if (noBread)
    		{
    			buy bread;
    			remove Note;
    		}
    	}
    }while(1);
    

    分析这个过程看得到,双方留下纸条后,进去检查if (noNote)都是不成立的,这样,两个人都不会去买面包,如果这种巧合一直按照这种顺序发生,那么永远不会有人买面包。

    • 方案三
      标签加以区别:
    do{
    	leave my Note;
    	if (no Other's Note)
    	{
    		if (noBread)
    		{
    			buy Bread;
    		}
    		remove my Note;
    	}
    }while(1);
    

    显而易见,依旧是不行的。

    • 方案四
    do{
    	leave Note1;
    	while (is Note2 Exist)					
    	{										
    		do nothing;							
    	}
    	
    	if (noBread)
    	{
    		buy Bread;
    	}
    	remove Note1;
    }while(1);
    
    do{
    	leave Note2;
    	if (noNote1)
    	{
    		if(noBread)
    		{
    			buy Bread;
    		}
    	}
    	remove Note2;
    }while(1);
    

    这个方案可行吗?可行。但是明显看的出和前面3个方案的不同,这里用了两段不同的代码。此方案有如下缺点:

    • 同样是2个人买面包,上面三种方案,购买流程一套代码即可实现,而此情况需要2套代码。
    • 如果处理线程数超过2个,处理逻辑的复杂度会呈指数级增长。
    • 并且第一段处理有一个死循环,如果下面一段的处理比较耗时,则上一段的死循环会持续很久,导致比较高的CPU占用。
    • 方案五
      既然方案四已经否定了,综合前三个解决方法来看,可以看出,主要是因为放下纸条和检查纸条这个过程被打断了,2个动作可以拆分为2次完成。如果通过一些手段把这两个动作绑定在一起,看看会是怎么样。
    do
    {
    	while(check_and_leave());	//执行不可打断
    
    	if (noBread)
    	{
    		buy Bread;
    	}
    
    	remove Note;
    }while(1);
    

    check_and_leave()里面的逻辑是:

    if (noNote)
    {
    	leave Note;
    	return FALSE;
    }
    else
    {
    	return TRUE;
    }
    

    这样就可以解决竞态问题了。只要实现check_and_leave()执行动作的不可打断就可以了。

    4.临界区

    4.1概念

    enter section;
    	critical section;
    exit section;
    	remainder section;
    

    临界区(critical section):

    • 任务执行时,需要互斥的一段区域。

    进入临界区(enter section):

    • 进入临界区需要先检查是否有人已进入临界区。
    • 如果可进入临界区,设置进入标志。

    退出临界区(exit section):

    • 清除进入临界区标志。

    4.2访问规则

    • 空闲则进入
      没有任务进入了临界区,任何任务都可以进入。
    • 繁忙则等待
      有任务进入临界区,其他任务都得等待。
    • 有限等待
      未进入临界区的任务不能无限等待。
    • 让权等待(可选)
      未进入临界区的任务,应释放CPU使用权(如切换到阻塞态)。

    4.3如何实现临界区的访问

    4.3.1禁止中断

    禁止中断相当于是硬件方法实现,在第三节的分析可知,临界区出现了竞态主要是因为任务调度引起,或者中断引起。而任务调度也是由中断方法实现(如时间片,超时则引发调度,由时钟中断导致),所以禁止了中断,可以实现临界区的访问。

    local_irq_save(unsigned int flags);
    critical section;
    local_irq_restore(unsigned int flags);
    

    进入临界区(enter section):

    • 禁止所有中断,保存CPSR。

    退出临界区(exit section):

    • 使能中断,恢复CPSR。

    缺点:

    • 禁止中断后,执行的任务将无法响应任何外部的输入,例如信号,中断,一直执行下去。
    • 如果临界区执行耗时较长,那么对系统的性能有很大的影响。
    • 对SMP情况是无法处理的,因为禁中断只会禁止当前程序运行的CPU上的中断。

    因为这些局限性,要小心使用禁用条件中断。

    4.3.2软件方法

    4.3.3高级抽象

    基于硬件原子性操作的高级抽象方法。硬件提供了一些原语,像中断禁用、原子操作指令等,操作系统在此基础上提供更高级的编程抽象来简化并行编程,例如锁、信号量,这些方式是从硬件原语中构建的。

    那么如何实现这种抽象呢?

    Test And Set

    bool Test_And_Set(bool* flag)
    {
    	bool rv = *flag;
    	*flag = TRUE;
    	return rv;
    }
    

    这是一条机器指令,这条机器指令完成了通常操作的读写两条机器指令的工作,完成了三件事情:

    • 从内存中读取值
    • 测试该值是否为1(然后返回真或假)
    • 内存值设置为1

    Exchange

    void Exchange(bool *a, bool *b)
    {
    	bool tmp = *a;
    	*a = *b;
    	*b = tmp;
    }
    

    虽然这两个指令看起来由几条小指令组成,但是它已经被封装成了一条机器指令,这就意味着它在执行的时候不会被打断,不允许出现中断或切换,这就是机器指令的语义保证。在此基础上完成互斥。

    使用Test_And_Set实现自旋锁

    这里看到如果锁状态value为1,表示临界区现在被人占用,已经上锁了,这时调用Lock::Acquire()不能得到锁,那么它会在这里死循环,不断测试value的值。这种空转全用来消耗任务的时间片,显然是可以有优化空间的。例如发现获取锁失败,则将任务置为睡眠状态,挂入到等待队列。

    忙等和非忙等各有各的使用场景,如果临界区比较简单,执行不会消耗多少时间,那么用忙等更合适,因为非忙等是需要进行任务调度的,调度出去,然后再调度进来,这个调度的开销不能白白浪费在适合忙等的临界区。

    使用exchange实现锁:

    class Lock{
    	int value = 0;
    }
    
    Lock::Acquire(){
    	int key = 1;
    	while(1 == key){
    		exchange(lock, key);
    	}
    }
    
    Lock::Reease(){
    	value = 0;
    }
    

    不管是Test_And_Set还是Exchage他们的终极目的其实都是在一个机器指令里面对一个变量实现:

    • 读取当前值
    • 将变量设置为1
  • 相关阅读:
    win7通过配置怎么样ODBC数据源
    apache2.2 虚拟主机配置
    Matlab曲面拟合和插值
    查询记录rs.previous()使用
    阅读《平庸的世界》后感觉 (两)
    苹果公司的新的编程语言 Swift 高级语言()两--基本数据类型
    UVA 12075
    [LeetCode] Search a 2D Matrix [25]
    js 数组,字符串,JSON,bind, Name
    linux系统下怎么安装.deb文件?
  • 原文地址:https://www.cnblogs.com/thammer/p/12584712.html
Copyright © 2011-2022 走看看