动手学树莓派第4章:多任务,原来如此简单

前后设计模式的不利之处

如果之前您做过无操作系统的嵌入式开发,使用“前台main——后台中断”这种前后台模式开发。

例如,任务1,你要完成接收串口来的1000字节数据,接收完毕数据后,进行校验,校验正确后,把所有数据乘以100,再从串口将数据发送(暂不考虑异常时处理);任务2,你要完成从网口接收1000字节数据,进行校验,校验正确后,把所有数据乘以100,再从网口将数据发送(暂不考虑异常时处理)。

对于上述要求的任务,常规逻辑就是:在串口中断中,将接收到的数据放入数据缓冲区中,当接收完毕1000字节后,置串口数据接收完毕标志,程序代码main中的while(1)循环中查询串口接收标志有效,怎进行数据的校验和乘以100的处理;同理,网络接收数据也是这样处理,中断中接收数据、置标志,主循环中数据处理。C语言伪代码如下:

//串口中断处理函数

void uart_irq(void){

//从串口寄存器中读数据,并放在uart_data[]缓冲区中
uart_data[uart_data_len] = uart_rev_register;
uart_data_len ++;

//接收到UART_REV_LEN(当前要求为1000字节),则置标志有效
if(uart_data_len >= UART_REV_LEN){
    uart_rev_flag = TRUE;
    uart_data_len = 0;
}
}

//网口中断处理函数

void net_irq(void){

//从网口寄存器中读取数据,并放在net_data缓冲区中
for(temp_i = 0; temp_i < net_len_reg; temp_i ++){
    net_data[net_data_len] = net_data_register;
    net_data_len ++;
}

//则置标志有效 
net_rev_flag = TRUE;
uart_data_len = 0;
}

void main(void){

while(1){

    //串口接收标志有效
    if(uart_rev_flag == TRUE){

        //置标志为无效,用于下次数据接受
        uart_rev_flag == FALSE;

        //调用串口数据处理函数
        uart_data_process_func();
    }

    //网口接收标志有效
    if(net_rev_flag == TRUE){

        //置标志为无效,用于下次数据接受
        net_rev_flag == FALSE;

        //调用网口数据处理函数
        net_data_process_func();
    }
}
}

如果你的项目要求20个任务,那你的main会很庞大,但也能做;如果,部分任务之前有任务的紧急程度不同,那用这种“前后模式”设计程序,非常考研设计的任务规划能力,再加上后续用户不断的新增新的任务,那经过若干次修改后,程序简直不忍直视。

忠告:

不要小看了用户修改,我有个项目,历经5年时间,修改了无数的版本;要记住一句忠告“用户虐我千百遍,我待用户如初恋”,用户永远是上帝。

让LED和数码管尽情的跳动吧

必备工具:请将NXEZ瑞士军刀开发板,安装到树莓派上,如下所示

如此复杂的任务,简单解决

设计任务:任务1,LED灯按照流水灯方式进行点亮;任务2,数码管显示程序运行的时间,单位:秒。任务和简单吧,你可以想想用前后台模式怎么设计,是不是要开个定时器,在主循环中一会变led的状态,一会变数码管的状态,好费经吧。

那让我们借助操作系统的多任务这个工具吧;看吧,是不是很简单:)。

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

#流水灯每个灯点亮时间
WATERLIGHT_DELAY = 0.1

#数码管刷新显示数字的延时
DIGSEC_DELAY = 0.7

#Declare the SAKS Board
SAKS = SAKSHAT()

#流水灯任务定义
def waterlight(task_name, delay_ts):
    print(task_name + "任务启动")
    try:
        while True:
            for i in range(0,8):
                SAKS.ledrow.on_for_index(i)
                time.sleep(delay_ts)
                SAKS.ledrow.off_for_index(i)
    except KeyboardInterrupt:
        print(task_name + "任务被终止")
        
#数码管显示秒值
def digital_second(task_name, delay_ts):
    print(task_name + "任务启动")
    #记录开始时间
    start_time = datetime.utcnow()
    try:
        while True:
            end_time = datetime.utcnow()
            c = end_time - start_time
            #print c.seconds
            #print c.microseconds
            SAKS.digital_display.show(("%02d.%02d" % (c.seconds, c.microseconds)))
            time.sleep(delay_ts)
    except KeyboardInterrupt:
        print(task_name + "任务被终止")

if __name__ == "__main__":
    try:
        #创建led和数码管显示闪烁任务
        led_flash = Process(target=waterlight, args=("流水灯", WATERLIGHT_DELAY))
        digsec = Process(target=digital_second, args=("数码管显示", DIGSEC_DELAY))
        
        # 启动任务
        led_flash.start()
        digsec.start()

        #等待启动的进程执行结束
        led_flash.join()
        digsecjoin()

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

为什么要使用multiprocessing模块

python提供了os.fork创建进程,提供了thread模块创建线程,这不是足够了吗?

这是不够的,os.fork无法在windows实现,不具有可移植性;线程虽然可以,但由于python的GIL(Global Interpreter Lock,全局解释器锁)的存在,同一时刻python只能运行一个线程,没法将多线程分配到多核上运行。

这就是引入了multiprocessing模块的原因,解决了windows移植到问题和如何利用多核的问题。

如何使用multiprocessing模块呢?就3步。

1.定义自己的任务,

#流水灯任务定义
def waterlight(task_name, delay_ts):
    print(task_name + "任务启动")
    try:
        while True:
            for i in range(0,8):
                SAKS.ledrow.on_for_index(i)
                time.sleep(delay_ts)
                SAKS.ledrow.off_for_index(i)
    except KeyboardInterrupt:
        print(task_name + "任务被终止")

2.将任务交给multiprocess模块,

#创建led闪烁任务

led_flash = Process(target=waterlight, args=("流水灯", WATERLIGHT_DELAY))

1.启动该任务。

# 启动任务

led_flash.start()

看吧,就3步,是不是很简单。

进程的状态

从大的方面来说,进程在运行过程中有3种状态:就绪态、运行态、阻塞态。

(1)就绪态。
就绪态:进程具备运行条件了,可以被cpu执行了;但还在就绪队列中排着队。

例如,我们led灯例程中有两个地方是就就绪态的。
1.进程名.start()执行后,进程进入就绪态了。
2.time.sleep()之后,点亮灯(或熄灭灯)之前,进程进入就绪态了。

(2)运行态。
运行态:进程就是在cpu上运行了。
例如,执行把运行灯操作语句,把灯点亮(或者熄灭)时,这是就是运行态。

(3)阻塞态。
阻塞态:进程在等待某个事件发生的过程中,就是阻塞态;此时,一直在阻塞队列中等待着。
例如,执行time.sleep语句,进行延时时。

多任务好吧,你把做的事情告送操作系统,操作系统帮你进行管理、调度。

对比下,multiprocessing模块和thread模块的效率
下属代码及结论,摘抄自“莫烦python”, 链接:https://morvanzhou.github.io/tutorials/python-basic/multiprocessing/4-comparison/

结论:发现多核/多进程最快,说明在同时间运行了多个任务。 而多线程的运行时间居然比什么都不做的程序还要慢一点,说明多线程还是有一定的短板的。

更多关于multiprocessing的介绍运行下面代码

help("multiprocessing")

注意

本课程不是python语言语法学习课程,因为现在网上有大量优质的python语言语法学习课程,同学们可以自行选择免费的或收费的python语法课程,学习python本身。

本课程定位于:如何使用python理解树莓派、理解操作系统。放心由于python语言的通俗易懂,加上本课程都是最基本的python,及时没有基础,也可以快速理解,记住,咱们的课程是可以边学边练、随意修改、还有恢复机制,放心折腾吧。

置身事外看编程语言

咱们不探究python语言本身,那咱们探究下何为编程语言。(下面是个人理解,咱们可以敞开讨论)

编程语言的目的就是——把我们要做成的事情,翻译成计算机能理解的方式,告送计算机,由计算机帮我们实现。那咱们看看我总结的编程语言三大组成部分:
(1)变量。就是咱们要处理的信息,例如:温度数据、车的速度,楼的高度。变量可以继续划分,但划分的目的就是存储数据的大小,例如8位(2的8次方)变量可以存储0~255数字量,32位数据(2的32次方)存储0~4294967295。还有16位,64位、或者像python中不限制大小的变量。
(2)程序结构。就是咱们处理信息所使用的流程。有顺序结构、条件结构、循环结构。顺序结构很容易理解,就是依次处理数据,例如,我们依次进行,获取温度数据、获取汽车速度;条件结构,就是需要对信息进行判断,然后再处理,例如,获取完毕汽车速度后,判断汽车超速,就进行刹车;循环结构,就是不断地重复相同的事情,例如,我们每隔1s采集一次温度,就是不断循环的进行采集温度、睡眠1s、再采集温度、睡眠1s…。
(3)系统和第三方提供的库。其实有了变量和程序结构,就可以完成咱们要的工作,但是如果事必躬亲的话,估计咱们还处在原始社会。后人进步总是站在前人的肩膀上继续攀登。所以,前人已经他们已完成的工作,打包好,前人已完成的功能,我们理解后直接用,我们专注于自己新的工作。系统和第三方库,就是我们可以使用的现成工具,我们合理利用这些工具,重新组合完成我们新的需求,如果您做的好,您可以将您的工作封装成第三方库,留给后人使用。

这是我理解的编程语言的框架,您可以先按照我的方式看语言,或者自己建立自己语言框架。

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

返回课程目录

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