多线程—实现原理

多线程实现原理详解

概述

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

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

多线程实现原理

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.stopthread(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 锁,由系统调度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值