动手学树莓派第6章:停下来,别争了,咱们定个规矩

临界资源为何物

先看看百度百科给出的解释:
临界资源:多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。

举个通俗点的例子:一桌人吃饭(中国作为美食大国,拿吃举例是最形象的,我就是的吃货),上了一大碗“海鲜全家福”(最爱里面的扇贝丁和虾仁),只有一个公用勺(要讲餐桌礼仪,只能用公用勺取菜),这个公用勺就是“临界资源”,A拿了公用勺后,B、C、D等诸位只能等着,不然群起而拿之,一个公用勺同时有4只手,这场面不忍直视。

对于公用勺(临界资源)的使用我们要有保护,我们用餐桌礼仪约定(进程间同步的锁)来规范公用勺(临界资源)的使用。

上述够形象吧:)

锁?你说的是大门锁吗

当然不是了,但起到的作用和大门锁一致——你想进房间,先拿到钥匙,用完房间后,把钥匙归还;在这期间,其他人要用房间,只能等着,拿到钥匙才能继续使用。这里的房间就是“临界资源”。

操作系统为我们提供了哪些“临界资源”的保护机制。

操作系统为我们提供的保护机制就是锁。这个锁包含:互斥锁、读写锁、自旋锁。
(1)互斥锁。同一时刻,只能有1个进程访问临界资源。
(2)读写锁。多了读临界资源的进程可用同时访问,但写临界资源的进程一旦访问,所有使用临界资源的进程都要等待着该进程结束后,才能访问临界资源。
适用场合:预估您的系统中,对该临界资源的读操作次数大于写操作的次数,此时读写锁就可以带来比互斥锁更好的性能。
读写锁存在的原因:读操作不改变临界资源的值,读操作相当与您看看临界资源,看也不会咋样,所有可以由多个进程访问;但写就不一样,一旦写后,临界资源就被改变了,所有写操作由只能一个进程来进行。
(3)自旋锁。只在内核中使用,而且只在有多核处理器的情况使用。
自旋锁,就是一直死等着临界资源,同时占着CPU不放,就在原地跑圈(也不怕自己转晕了:))。

在使用互斥锁、读写锁时,等待锁期间,该进程是被拉入锁的等待队列中的,进程处于睡眠态,其他进程被拉入CPU进行操作;但使用自旋锁时,不释放CPU,就在那空等;

那为什么要引入自旋锁呢? 就是效率问题,如果临界资源很快可使用,此时,获取到了自旋锁使用权限,继续执行后续代码;但使用互斥锁和读写锁则引入进程(或内核线程)频繁的切入、切出CPU的运行队列,耗费了大量时间,降低了系统的性能;这就是自旋锁适用的场合。

Multiprocessing模块提供的进程间同步机制

multiprocessing支持进程同步机制——锁(互斥锁)。同一时刻只有一个进程可以获取锁。

help("multiprocessing.Lock")

我不在的时候,都争成啥样

任务要求:
(1)我们有个显示器,上面1s显示一次时间信息(咱们用数码管来显示进程已运行的时间)。
(2)我有重要信息要给给大家看时,必须显示该信息3s,这期间不能在显示时间了。(按键被按下后,表示重要信息来显示,显示9999)。

下面是没加锁的显示,怎么我要求的3s显示我的重要信息,没达到啊?都怪显示时间的进程,抢着要显示,这不这要信息看不见了,都怪他。此时,两个进程还在争…

from multiprocessing import Process
import time
import os
from sakshat import SAKSHAT
from sakspins import SAKSPins as PINS

#普通1s显示进程进程
def dig_normal_one_second(task_name,delay_second):
    print(task_name + "任务启动")
    try:
        while True:
            #延时等待显示下述信息
            time.sleep(delay_second)
            
            #显示普通信息
            SAKS.digital_display.show(("%04d" % (1234)))
            
    except KeyboardInterrupt:
        print(task_name + "任务被终止")
        
#重要10s显示进程
def dig_normal_ten_second(task_name,delay_second):
    print(task_name + "任务启动")
    try:
        while True:
            #延时等待显示下述信息
            time.sleep(delay_second)
            
            #显示重要重要信息9999显示2秒,6666显示2秒
            SAKS.digital_display.show(("%04d" % (9999)))
            time.sleep(2)
            SAKS.digital_display.show(("%04d" % (6666)))
            time.sleep(2)
            
    except KeyboardInterrupt:
        print(task_name + "任务被终止")
        
if __name__ == "__main__":
    try:
        #Declare the SAKS Board
        SAKS = SAKSHAT()

        #创建温度采集、本地显示温度、远程发送温度、本地温度存储进程
        one_second_prog = Process(target=dig_normal_one_second, args=("普通1s显示进程", 1))
        ten_second_prog = Process(target=dig_normal_ten_second, args=("重要10s显示进程", 10))
        
        # 启动任务
        one_second_prog.start()
        ten_second_prog.start()

        #等待启动的进程执行结束
        one_second_prog.join()
        ten_second_prog.start()

    except KeyboardInterrupt:
        print("任务被终止了")

我来了,都别争了,咱们定个规矩

好了,别争了,我来定规矩,我把显示屏锁起来了,这又把钥匙,拿到这把钥匙,打开显示屏锁,显示你的信息,用完显示屏后,把显示屏锁起来,钥匙归还。记得一定要归还钥匙啊。

from multiprocessing import Process, Lock
import time
import os
from sakshat import SAKSHAT
from sakspins import SAKSPins as PINS

#普通1s显示进程进程
def dig_normal_one_second(task_name,delay_second, display_lock):
    print(task_name + "任务启动")
    try:
        while True:
            #延时等待显示下述信息
            time.sleep(delay_second)
            
            #延时等待显示下述信息
            display_lock.acquire()
            
            #显示普通信息
            SAKS.digital_display.show(("%04d" % (1234)))
            
            #释放显示锁
            display_lock.release()
            
    except KeyboardInterrupt:
        print(task_name + "任务被终止")
        
#重要10s显示进程
def dig_normal_ten_second(task_name,delay_second, display_lock):
    print(task_name + "任务启动")
    try:
        while True:
            #延时等待显示下述信息
            time.sleep(delay_second)
            
            #获取显示锁
            display_lock.acquire()
            
            #显示重要重要信息9999显示2秒,6666显示2秒
            SAKS.digital_display.show(("%04d" % (9999)))
            time.sleep(2)
            SAKS.digital_display.show(("%04d" % (6666)))
            time.sleep(2)
            
            #释放显示锁
            display_lock.release()
            
    except KeyboardInterrupt:
        print(task_name + "任务被终止")
        
if __name__ == "__main__":
    try:
        #Declare the SAKS Board
        SAKS = SAKSHAT()
        
        disp_lock = Lock()

        #创建温度采集、本地显示温度、远程发送温度、本地温度存储进程
        one_second_prog = Process(target=dig_normal_one_second, args=("普通1s显示进程进程", 1, disp_lock))
        ten_second_prog = Process(target=dig_normal_ten_second, args=("重要10s显示进程", 10, disp_lock))
        
        # 启动任务
        one_second_prog.start()
        ten_second_prog.start()

        #等待启动的进程执行结束
        one_second_prog.join()
        ten_second_prog.start()

    except KeyboardInterrupt:
        print("任务被终止了")

锁虽好用,使用不当,身受重伤

使用锁不当最容易出的两个问题就是:死锁和优先级反转,死锁是最致命的。
(1)死锁。什么情况会产生死锁呢,A进程此时手持锁1,在等待锁2;B进程此时手持锁2,在等待锁1;看吧,都在等着对方释放当前手持的锁,对于对峙中(这就叫死锁)。
(2)优先级反转。低优先级A进程此时手持锁1,在执行自己的任务;此时高优先级B进程变为运行态,要拿锁1,继续执行;但低优先级A进程未执行完毕,一直持有者锁1,这期间低优先级A进程,被中优先级C、D、E…进程抢了CPU,低优先级A进程一直处于睡眠态,等待这些中优先级任务执行完毕后,才执行自己的任务,待自己的任务执行完后,释放了锁1;此时,被遗忘很久的高优先级B才执行,B很伤心,我这么高的优先级,怎么被拖到现在才执行,都是锁惹的祸。看吧这就叫优先级反转,高优先级的进程被“降到”低优先级运行了。

那有结局措施吗?当然有了,措施就是:留待python下篇再讲。

稍事休息,继续攀登

至此,我们已经掌握了操作系统为我们提供的3大利器——多任务、进程间通信、进程间同步,我们可以按照自己的方式将任务合理的分解为多任务方式实现了,让程序设计变为一种美的设计。

下面我们开始玩耍我们NXEZ扩展板了,对于每种外设,介绍每种外设对象提供的方法。依照创客百科中”SAKS SDK 开发文档“进行讲解和实验。

课程 bilibili 视频地址:https://www.bilibili.com/video/av71878718/?p=16

返回课程目录

课程 gitee 地址:https://gitee.com/shirf_taste_raspi/shirf_serial_share