操作系统之进程与线程4

进程与线程-同步

  • 屏蔽中断

    • 单处理器,最简单的方式是在进入临界区后立刻屏蔽所有中断,离开时再打开中断,屏蔽中断后时钟中断也被屏蔽,cpu不会进行进程切换。
    • 此种方式很多弊端,若不再打开中断、多处理器屏蔽中断指令只对一个cpu生效其他cpu仍可以进入临界区,因此不太适用。
  • 忙等的算法

    • 以下介绍的三种竞争加锁方式都是需要忙等的。他们在未获得锁而进入临界区时需要一直原地循环等待,这样浪费了CPU时间,还可能发生优先级反转问题。

    • 优先级反转问题:假设线程之间存在优先级,优先级高的线程只要处于就绪状态就一直运行而不交出cpu。若某时刻优先级低的线程处于临界区内而优先级高的线程正在cpu中执行忙等操作,此时优先级高的线程因为忙等不会交出cpu,而优先级低的线程因为无法获得cpu去执行完临界区中的操作所以就无法交出锁,因此会导致死锁。

    • 严格轮换算法

      • 严格轮换算法可以控制两个线程访问一个共享的单用户资源而不发生访问冲突。但存在严格的轮换执行顺序。

        int turn=0;//运行权限的线程id
        void enter(int process){
            //如果turn不为当前线程则一直循环等待
            while(turn != process);
        }
        void leave(int process){
            //将运行权限交给另一个线程
            turn = 1-process;
        }
        
      • 严格轮换算法有一个显著的缺点就是一个线程在获得了资源并使用完毕离开后会将权限交给另一个线程,而另一个线程如果没有在这之后获得了资源并使用完毕离开则此线程就不能再一次获得资源。

      • 因此严格轮换算法的获得资源顺序必须是0-1-0-1这种严格的依次获得的顺序。

    • Peterson算法

      • Peterson算法是一个实现互斥锁的并发程序设计算法,可以控制两个线程访问一个共享的单用户资源而不发生访问冲突。

      • 而且相比于严格轮换算法它一定程度上避免了一个线程获得资源并离开后必须等待另一个线程获得资源并离开才能获得到资源的缺点。实现了按需争抢-一个线程只有将自己的竞争标志位设置了另一个线程才会等待它。

        #define TRUE  1
        #define FALSE 0
        int turn;  		//当前有权限的线程id
        int flag[2];	//是否竞争标志位
        void enter(int porcess){
            int other = 1 - process; //另一个线程号
            interested[process] = TRUE; //当前开始了竞争
            turn = other; //权限给对方线程
            //如果当前线程没有权限且对方的竞争标志位为true则等待。
            while(turn != process && flag[other] == TRUE);
        }
        void leave(int process){
            inserested[process] = FASLE;//取消了竞争
        }
        
      • 通过代码可以看出只有

        • 1对方线程没有在自己的竞争标志位设置为true时
        • 2对方线程执行了turn=other时将权限交给了本线程时
        • 3对方线程先获得了资源之后在离开资源时将自己的竞争标志位设置为false时
        • 这三种情况之一满足时才可以获得到资源而不是循环等待。
      • T0执行,T2还未执行

        T0 T1
        flag[0]=true 没执行
        turn=1 没执行
        未进入循环,获得了资源 没执行
      • T0执行时,T1也执行了

        T0进入循环判断前,T1将自己的竞争标志位置为true,导致T0一直循环等待直到T1执行了turn=0将权限交给T0时,T0才结束循环获得到了资源。

        T0 T1
        flag[0]=true flag[1]=true
        turn=1 没执行
        没有权限循环等待 没执行
        没有权限循环等待 turn=0
        获得权限,结束循环获得了资源 没有权限循环等待
        flag[0]=false取消了竞争 没有权限循环等待
        没执行 对方了取消了竞争,结束了循环获得了资源
    • TSL和XCHG指令

      • TSL和XCHG提供了原子性读取并更新的功能,利用这些指令可以很方便的实现多线程竞争锁的程序功能。

      • TSL(测试并加锁指令),TSL RX,LOCK,它将一个LOCK地址的内存字读到寄存器RX中,并在该内存地址上存一个非零值。读与写是可以保证原子性的。执行该指令的CPU会锁住内存总线以禁止其他cpu访问此地址。

        enter_region:
        	;复制LOCK地址存的值到REGISTER寄存器上
        	;并将LOCK地址的值设置为1
        	TSL REGISTER,LOCK 
        	CMP REGISTER,#0  ;判断取出的值是否为0
        	JNE enter_region ;不是0则代表锁已被占有,继续循环
        	RET ;获得到了锁,返回调用者
        leave_region:
        	MOVE LOCK,#0 ;向LOCK地址存入0
        	RET          ;返回调用者
        
      • XCHG(原子性交换指令),XCHG RX,LOCK,它将LOCK地址的内存字与RX寄存器中的字进行交换。它也可以代替TSL,所有Intel x86底层同步使用的都是XCHG指令。

        enter_region:
        	MOVE REGISTER,#1   ;寄存器中放个1
        	XCHG REGISTER,LOCK ;交换LOCK与寄存器的数值
        	CMP REGISTER,#0    ;判断LOCK之前存的是否为0
        	JNE enter_region   ;不是0则说明锁已经设置,循环等待
        	RET ;获得了锁,返回调用者
        leave_region:
        	MOVE LOCK,#0  ;在LOCK处存入0
        	RET           ;返回调用者
        
  • 线程阻塞唤醒实现不忙等

    • sleep和wakeup系统调用,可以使线程在无法获得锁时进入阻塞状态,而不是cpu忙等待,避免了cpu资源浪费。

    • 信号量

      • 信号量是通过 一个整型变量来累计唤醒次数。分为down和up操作。
      • down操作在信号量>0时使信号量减一,信号量=0时线程休眠。
      • up操作在信号量在没有线程休眠时使信号量加一,否则随机唤醒一个线程。
      • 注:对于信号量变量的操作必须是原子性的。
    • 互斥量

      • 如果不需要信号量的计数功能,可以使用其的一个简化版本-互斥量。

      • 互斥量分为加锁和解锁两个操作,只需要一个二进制位即可表示。

      • 如果调用加锁时,如果其他线程已经获得锁了则该线程阻塞。

      • 获得锁的线程在解锁时会随机唤醒一个因为无法加锁而阻塞的线程。

        ;一种用户级线程的解决方案
        mutex_lock:
        	TSL REGISTER,MUTEX ;互斥量复制到寄存器中,互斥量置1
        	CMP REGISTER,#0 ;判断复制出来的互斥量是否为0
        	JZE ok ;是0则获得了锁直接结束
        	CALL thread_yield ;不是0则让出cpu资源
        	JMP mutex_lock ;阻塞恢复后回到开始重新尝试加锁
        ok: RET
        
        mutex_unlock:
        	MOVE MUTEX,#0 ;将互斥量值0
        	RET
        
    • pthread中提供的同步函数

      • Pthread提供了两种同步机制:互斥量、条件变量。

      • 互斥量

        • 互斥量(mutex)同时只可以被一个线程获得,而其他线程尝试获得失败时会阻塞直到互斥量被其他线程释放。

        • 提供了一系列调用

          线程调用 描述
          pthread_mutex_init 创建一个互斥量
          pthread_mutex_destory 撤销一个互斥量
          pthread_mutex_lock 获得互斥量或阻塞
          pthread_mutex_trylock 获得互斥量或失败
          pthread_mutex_unlock 释放互斥量
      • 条件变量

        • 条件变量允许线程因为一些未达到的条件而阻塞,等到其他线程做了工作使得条件满足时去唤醒阻塞的线程。

        • 提供了一系列调用

        线程调用 描述
        pthread_cond_init 创建一个条件变量
        pthread_cond_destory 撤销一个条件变量
        pthread_cond_wait 阻塞以等待一个信号
        pthread_cond_signal 向另一个线程发送信号唤醒它
        pthread_cond_broadcast 向多个线程发送信号唤醒它们
        • pthread_cond_wait函数传入一个mutex互斥量,在调用时它会原子性的释放解锁mutex,并把此线程加入到等待队列中,在被唤醒导致函数返回时它又会重新争抢以获得mutex锁。
      • 条件变量与互斥锁的一同使用的例子

        #include<pthread.h>
        int value=0;
        pthread_mutex_t mutex;
        pthread_cond_t cond;
        void provider(){
            lock(&mutex);
        	while(value<=0){
        		pthread_cond_wait(&cond,&mutex);
        	}
        	unlock(&mutex);
        }
        void consumer(){
            lock(&mutex);
        	if(value==0){
        		value++;
        	}
        	if(value>0){
        		pthread_cond_signal(&cond);
        	}
        	unlock(&mutex);
        }
        
      • 条件变量解决的是在一些需要等待条件满足的情况时,如果只使用互斥量则需要先加锁再判断条件是否满足,若不满足则需要释放锁并sleep或重复加锁判断。一直循环会造成cpu浪费,sleep再循环又因为等待条件满足而sleep的时间无法判断因此sleep时间可能会过长。

      • 因此在这种情况下利用用等待队列加上通知唤醒方式的条件变量就显得性能更加强劲了,它在条件不满足时会把线程加入到等待队列中,而其他线程的工作导致条件满足时可以唤醒一个等待队列中的线程。这样既避免了循环加锁判断带来的cpu浪费又避免了sleep时间过长的缺点。

发布了27 篇原创文章 · 获赞 1 · 访问量 892

猜你喜欢

转载自blog.csdn.net/hu853996234/article/details/105026004
今日推荐