QuecPython+多线程的应用指南

概述

多线程是指从软件或者硬件上实现多个线程并发执行的技术。在一个程序中同时执行多个线程,每个线程都可以执行独立的任务,可以让程序在执行阻塞操作(如I/O操作)时不会阻塞整个程序的执行,从而提高程序的效率。

QuecPython _thread 模块包含软件层线程操作相关的功能,提供创建、删除线程的方法以及互斥锁、信号量等相关的接口。并且 QuecPython 提供 queue、sys_bus、EventMesh等组件模块方便多线程业务处理。

多线程实现原理

QuecPython 本身并没有创建线程资源,QuecPython 中一个线程对应底层 RTOS 系统中一个线程,依赖于底层的线程调度。那么底层是如何调度的进行多任务执行的?

线程创建

python 创建线程提供了较为便捷的创建方式,忽略了底层的栈大小配置,优先级等参数传递,尽量简化用户使用。python 创建线程时会在底层 RTOS 系统中生成对应的任务控制块(TCB),用于任务调度及线程资源控制。

协议栈大小默认配置 8k。并且也为客户提供了栈大小的配置,可以通过 _thread.stack_size() 接口对栈大小进行配置查询。

import _thread
import utime

# 线程函数入口,实现每隔一秒进行一次打印。
def thread_func_entry(id, name):
    while True:
        print( 'thread {} name is {}.'.format(id, name))
        utime.sleep(1)

# 创建线程
_thread.start_new_thread(thread_func_entry, (1, 'QuecPython'))

线程状态

线程有着自己的生命周期,从创建到结束,总是处于下面五个状态之一:新建状态(creation)、就绪状态(runnable)、运行状态(running)、阻塞状态(blocked)、终止状态(dead)。

  • 新建状态(creation):创建线程,实现线程可运行状态初始化。
  • 就绪状态(runnable):处于这个状态的线程位于可运行池中,等待获得 CPU 的使用权。
  • 运行状态(running):当就绪状态中的线程获得了 CPU 执行资源,执行线程函数中的代码,这样的线程我们称为运行状态的线程。
  • 阻塞状态(blocked):处于运行中的线程,由于某种原因放弃对 CPU 的使用权,处于阻塞状态,此时线程被挂起,不再执行,直到其进入就绪状态,才有机会再次被 CPU 调用进入运行状态。这种阻塞状态可能由多种原因导致,比如调用 sleep,信号量,锁等方式。
  • 终止状态(dead):线程在完成执行或异常中止时进入终止状态。

线程调度机制

多线程同时执行是伪命题,实际多线程并不是所有线程都能一直运行,一直独占 CPU,不论硬件资源多强大,对于创建成千上万个线程,都是需要一定的调度算法实现多线程的,那么多线程的调度机制是如何实现的呢?

在 RTOS 系统中,常见调度机制包括时间片轮询调度、基于优先级的协同调度、抢占式调度。一般在 RTOS 系统中,包含多种调度算法结合使用。

时间片轮询调度

RTOS 中的轮询调度策略,是允许多个任务可以分配同一个优先级别。调度程序基于 CPU 时钟监控任务时间,任务处于相同优先级,按照先进先出的原则执行分配到的时间片,时间到了,即使当前任务还没有完成,任务也将 CPU 时间传递给下一个任务。在下一个分配到的时间段内,该任务将从它停止的位置继续执行。

如下图所示,根据 cpu tick 时间划分一个一个时间片,每个时间片结束后,会切换到下个就绪状态的任务执行,然后依次执行就绪状态任务A、B、C。

基于优先级的协同调度

RTOS 中的优先级协同调度,是基于优先级的非抢占调度方法。任务按优先级排序,并且是事件驱动类型的,一旦正在运行的任务完成,或者任务主动放弃 CPU 使用,就绪运行的优先级最高的任务才可以获得 CPU 使用权。

如下图所示,根据优先级任务调度方法,在执行任务 A 时,出现中断时间任务,会立马执行高优先级中断事件任务,高优先级中断执行完成或者让出 CPU 后,继续执行任务 A,在任务 A 完成后或者让出 CPU 后,再切换到较高优先级任务 B。

抢占式调度

RTOS 通过可抢占调度保证实时性。为了保证任务响应,在抢占调度策略中,只要一个优先级更高的任务就绪,正在运行的任务低优先级任务将被切换出来。通过抢占,正在运行的任务被迫放弃 CPU,即使任务工作还没有完成。

如下图所示,根据抢占式调度方法,执行任务 A 时,出现优先级高的任务 C,会立即切换到任务 C 执行,在高优先级任务 C 执行完成或者让出 CPU 后,根据当前就绪状态的线程任务中找优先级较高的任务执行,此时执行任务 B,任务 B 执行完成或者让出 CPU 后,再继续执行任务 A。

线程上下文切换

实际多线程运行中,总是通过不断切换来保持多个线程同时运行的,那么对于多线程任务调度切换过程是如何进行的?切换后如何恢复运行?

当操作系统需要运行其他的任务时,操作系统首先会保存和当前任务相关的寄存器的内容到当前任务的栈中,然后从将要被加载的任务的栈中,取出之前保存的全部寄存器的内容并加载到相关的寄存器中,从而继续运行被加载的任务,这个过程叫作线程上下文切换。

线程上下文切换会带来额外的开销,包括对线程上下文信息保存和恢复的开销,对线程进行调度的 cpu 时间开销以及 cpu 缓存失效的开销。

线程清除

线程是系统最小的调度单位,系统线程创建及释放需要对应的资源创建及清除。

QuecPython 为简便客户使用,在线程运行结束后,会自动释放线程资源,可以无需关心线程资源释放问题,对于需要在其他线程控制某线程关闭,可以通过 _thread.stop_thread(thread_id) 接口,根据 thread_id 来控制指定线程。

通过 _thread.stop_thread(thread_id) 暴力关闭线程,释放线程资源,需要注意对应线程是否有锁、内存申请等需要用户释放的相关操作,防止导致死锁或者内存泄漏情况。

import _thread
import utime

# 线程函数入口,实现每隔一秒进行一次打印。
def thread_func_entry(id, name):
    while True:
        print( 'thread {} name is {}.'.format(id, name))
        utime.sleep(1)

# 创建线程
thread_id = _thread.start_new_thread(thread_func_entry, (1, 'QuecPython'))

# 延时 10 秒后删除每秒打印线程。
utime.sleep(10)
_thread.stop_thread(thread_id)

QuecPython 多线程处理

QuecPython 多线程依赖于底层系统的调度方式,并且在此基础上增加 GIL 锁实现多线程。

python 是解释器语言,对于多线程处理需要按顺序执行,用来保证进程中同一个时刻只有一个线程在执行,因此引入 GIL 全局锁概念,防止因为多线程状况下引起共享资源异常问题。

QuecPython 线程在系统基础上定义了主线程、python 子线程、中断/回调线程,并固定其优先级,其主线程(repl交互线程)优先级 < python 子线程 < 中断/回调线程。

如下图所示,QuecPython 多线程处理切换过程,执行任务 A 时,在任务 A 释放 GIL 锁后,切换到优先级高的中断任务 C,在高优先级任务 C 释放 GIL 后,执行优先级较高的任务 B 并加 GIL 锁,任务 B 执行释放 GIL 锁后,再继续执行任务 A。QuecPython 避免 GIL 锁导致高优先级任务无法执行及多线程调度灵活性,在执行一定次数后会自动释放 GIL 锁,由系统调度。

线程间通信&资源共享

线程间通信指至少两个进程或线程间传送数据或信号的一些技术或方法。在多线程中使用中,线程间通信必不可少,通过线程间通信控制线程运行,共享资源控制、消息传递等,实现程序的多样化。

线程间通信适用场景资源消耗
互斥锁用于信号传递,不可数据传递,常对于多线程公共资源竞争进行保护,控制程序运行。使用资源消耗较少。
信号量用于信号传递,不可数据传递,常对于多线程公共资源竞争进行保护,控制程序运行。相比较互斥锁更加灵活。使用资源消耗较少。
共享变量数据传递,常搭配互斥锁、信号量使用。使用资源消耗较少。
消息队列用于信号传递,数据传递,适用于生产者-消费者模型。使用资源消耗中等。
sys_bus数据传递,一对一通信或者一对多通信,基于发布/订阅范式的数据协议。异步发布数据使用资源较大。
EventMesh数据传递,一对一通信,基于发布/订阅范式的数据协议。异步发布数据使用资源较大。

 详细使用方法可查看:多线程 - QuecPython

常见问题

  1. 线程创建失败

    一般情况下,创建线程失败基本都是由于内存不足导致的。可以使用_thread.get_heap_size()确认当前内存大小,并且通过调整线程栈空间的方式,尽量节省内存消耗,当然栈空间需要满足使用空间需求,否则会出现dump。

  2. 线程资源如何释放防止内存泄漏

    当前线程退出可以通过两种方式,通过接口_thread.thread_stop(thread_id),可以直接在外部中断程序运行。或者线程自动运行结束退出,系统会自动回收线程资源。

  3. 线程死锁问题

    死锁(英语:deadlock),当多线程情况下,双方都在等待对方停止执行,以获取系统资源,但是没有一方提前退出时,就称为死锁。

    需要保证线程锁成对使用,加锁后不应该有较多任务,避免锁内再掉锁等情况, 并且_thread.thread_stop(thread_id)慎用,防止出现加锁过程中,退出程序导致死锁。

  4. 如何唤醒阻塞线程

    对于需要阻塞的线程,可以通过线程间通信的方式实现,能够唤醒的方式进行阻塞,不要采用sleep等方式,无法唤醒。

  5. 优先级

    QuecPython 固定优先级,当前主线程(repl交互线程)优先级 < python 子线程 < 中断/回调线程。用户创建所有的 python 子线程优先级同级。

  6. 如何进行线程保活

    当前尚未提供 python 守护线程,或者线程状态查询接口,如果需要保证线程存活,可以自定义保活机制,比如根据需要保活的线程的使用情况,进行计数统计,保证线程在一定时间内会执行某个动作,若未完成,则进行线程清除及重新创建。

  7. 线程死循环

    当前QuecPython适配较多平台,对于线程死循环可能会导致业务无法进行,系统喂狗超时,导致dump。

  8. 线程栈耗尽

    QuecPython 不同的平台上,默认创建线程栈空间不同,默认2k/8k,当线程业务量较大时会导致栈溢出,会导致无法预知dump,因此需要考虑当前默认栈是否满足业务需要,通过_thread.thread_size()接口确认当前栈大小及设置栈大小。

  9. 线程安全

    线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且准确的执行,不会出现数据污染等意外情况。
    我们支持互斥锁、信号量等方式进行数据保护,在进行多线程共享数据时,用户可以根据需要进行使用。

  10. 中断、回调、python 子线程、主线程(repl交互线程)的优先级对比。

    中断/回调依赖与其触发线程的,不同的中断/回调具有不同的触发对象,因此无法确定其优先级。

    主线程(repl交互线程)优先级 < python 子线程 < 中断/回调线程。

  11. 相同优先级的是否支持时间片轮转。

    支持,但是是受限的,由于 python 层 GIL 锁机制,线程在调度后会添加 GIL 锁,只有保证 GIL 锁是空闲的才能执行成功,直到 python 线程执行结束或者让出 GIL 锁,其他线程才能会被执行。

  12. 是否支持线程优先级配置。

    不支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值