多线程¶
Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.
功能概述¶
多线程是指在软件或硬件层面实现多个线程并发执行的技术。基于该技术,每个线程都可执行独立的任务。当某个线程因执行阻塞操作(如I/O操作)而等待时,不会影响其他线程的运行,也不会阻塞整个程序,从而提高了整体执行效率。
UniRTOS模块提供软件层的线程操作功能,包括创建和删除线程的方法,以及互斥锁、信号量、消息队列、事件标志等同步与通信接口,方便多线程业务处理。
裸机系统与多线程概述¶
裸机系统概述¶
裸机系统主要分为轮询系统和前后台系统。轮询系统在主循环中顺序查询各任务状态并执行,结构简单但响应不及时。前后台系统由中断服务程序(前台)和主循环(后台)组成,中断服务程序用于处理紧急事件,后台循环完成非实时任务,从而提升了响应速度。两者均无操作系统调度,适用于资源有限的嵌入式场景。
轮询系统¶
轮询系统是一种裸机编程架构。程序在硬件初始化后进入主循环,依次执行各项任务,如LED控制、串口输出等。这种顺序执行模式适合流程固定的操作。然而,当需要处理外部异步事件(如按键)时,其缺陷显现:如果主循环正执行一个耗时任务,即便紧急事件发生,也必须等待该任务完成才能被检测,从而导致响应延迟甚至事件丢失。因此,轮询系统实时性较差,难以应对需要即时响应的场景。
int main()
{
//初始化系统所需要的资源
while(1)
{
TASK_FUNCTION1();
TASK_FUNCTION2();
TASK_FUNCTION3();
.......
}
return 0;
}
前后台系统¶
前后台系统在轮询架构基础上引入了中断机制,形成“前台中断响应+后台循环处理”的双层结构。外部紧急事件(如按键、通信数据接收)由中断服务程序作为“前台”快速响应,通常仅进行标志位设置或数据暂存;而耗时的具体处理流程则在“后台”,即主循环中通过轮询检测这些标志来完成。这种设计显著提升了系统对异步事件的实时响应能力:中断能立即捕获事件,确保关键操作不被遗漏。前后台系统兼顾了响应速度与逻辑清晰性,是许多实时性要求较高、但任务复杂度尚不足以引入RTOS的嵌入式应用的常见选择。
bool flag1=FALSE;
bool flag2=FALSE;
bool flag3=FALSE;
.....
int main()
{
//初始化系统所需要的资源
while(1)
{
if(flag1)
{
TASK_FUNCTION1();
flag1=FALSE;
}
if(flag2)
{
TASK_FUNCTION2();
flag2=FALSE;
}
if(flag3)
{
TASK_FUNCTION3();
flag3=FALSE;
}
.......
}
return 0;
}
EXTINT_HANDLER_ISR1()
{
flag1=TRUE;
}
EXTINT_HANDLER_ISR2()
{
flag2=TRUE;
}
EXTINT_HANDLER_ISR3()
{
flag3=TRUE;
}
多线程实现原理¶
线程基本架构¶
下图展示了一个单核CPU任务切换的基本框架。操作系统的内核设计质量,首先体现在任务调度机制的实现效率与可定制性上。以单核CPU的任务切换为基准框架,调度器的设计优劣通常取决于以下几个核心维度:任务配置策略(如优先级分配、阻塞延时处理、临界区保护、时间片轮询等)、任务数据结构的选择(单向链表、双向链表、树形结构或顺序表等),以及是否集成高级通信与同步原语(如消息队列、互斥锁、信号量等)。然而,一个完备的嵌入式操作系统不仅需要具备上述内核级能力,还需要对驱动层与应用层组件进行科学规划与系统集成。在驱动层面,需明确支持的外设接口类型(如UART、SPI、I2C等);在应用层面,则需根据目标场景合理选型并整合文件系统、网络协议栈、内存管理库等高阶功能组件。这些组件的选取与融合策略,直接决定了系统的可扩展性、硬件适配能力及最终产品的市场竞争力。上述所有关键组件均可依据实际资源约束(如程序存储空间、运行内存、实时性要求、客户需求等)进行定制化设计与实现,该设计思想亦可扩展至多核处理器架构。
任务控制块: 任务控制块(TCB)是内核为每一个任务创建和管理的私有数据结构,是操作系统的“任务管理中心”。
堆栈管理:
pxTopOfStack:指向该任务当前堆栈的栈顶,是最重要的指针之一。当任务被切换出去时,CPU寄存器的值保存在其栈中,该指针记录保存的位置。切换回来时,根据该指针恢复现场。
pxStack:指向该任务堆栈内存空间的起始地址(堆栈底部)。用于检查堆栈是否溢出。
usStackDepth:任务堆栈的总深度。
任务状态:阻塞、睡眠、执行、就绪和挂起。
任务属性:任务的优先级、任务名称及唯一标识符。
调度相关:互斥锁及其数量。
临界段:
在RTOS中,临界段是指访问共享资源(如全局变量)时,必须保证执行过程不被任何中断或任务切换所打断的代码段。由于系统的任务调度最终由PendSV中断触发,因此临界段的保护本质上是通过精确控制中断的使能与禁止来实现的,从而确保对关键资源操作的原子性和数据一致性。
阻塞延时:
当任务调用延时函数时,会主动放弃CPU使用权并进入阻塞态。 在此期间,调度器会从就绪任务中选择优先级最高的任务投入运行;若系统中无任何用户任务就绪,CPU将自动切换至最低优先级的空闲任务,以维持系统持续执行并通常会进入低功耗模式。直到延时时间结束,该任务才会重新进入就绪态,并可根据优先级策略竞争CPU使用权,从而继续执行。
线程状态¶
线程具有完整的生命周期,从创建到结束,始终处于以下五个状态之一:新建状态(creation)、就绪状态(runnable)、运行状态(running)、阻塞状态(blocked)和终止状态(dead)。
新建状态(creation):创建线程,实现线程可运行状态初始化。
就绪状态(runnable):处于该状态的线程位于可运行池中,等待获得CPU的使用权。
运行状态(running):处于就绪状态的线程获得CPU执行资源后,开始执行线程函数中的代码。
阻塞状态(blocked):运行中的线程因某种原因放弃CPU使用权而被挂起,不再执行,直到进入就绪状态后,才有机会再次被CPU调度执行。阻塞状态可能由多种原因导致,例如调用延时函数、等待信号量或互斥锁等。
终止状态(dead):线程在完成执行或异常中止时进入终止状态。
线程调度机制¶
多线程的绝对并行执行是一个伪命题。实际上,并不是所有线程都能一直运行并独占CPU。无论硬件资源多强大,对于成千上万个线程,都需要一定的调度算法来实现多线程。那么多线程的调度机制是如何实现的呢?
在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缓存失效的开销。
多线程API¶
头文件¶
qosa_sys.h
函数概览¶
函数 |
描述 |
|---|---|
qosa_task_create() |
创建标准版任务 |
qosa_task_create_static() |
创建静态版任务 |
qosa_task_delete() |
删除指定任务 |
qosa_task_suspend() |
挂起指定任务 |
qosa_task_resume() |
恢复被挂起的任务 |
qosa_task_get_current_ref() |
获取当前任务句柄 |
qosa_task_sleep_ms() |
设置任务休眠时间(单位:毫秒) |
qosa_task_sleep_sec() |
设置任务休眠时间(单位:秒) |
函数详解¶
qosa_task_create¶
功能描述
创建标准版任务。适用于无需任务间通信的简单场景。函数原型
int qosa_task_create(qosa_task_t* taskRef,
qosa_uint32_t stackSize,
qosa_uint8_t priority,
char* taskName,
void (*taskStart)(void*),
void* argv,
...)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
taskRef |
输出 |
qosa_task_t* |
指向任务句柄指针的地址,用于接收创建的任务句柄 |
stackSize |
输入 |
qosa_uint32_t |
任务堆栈大小。单位:字节 |
priority |
输入 |
qosa_uint8_t |
任务优先级,数值越大优先级越高 |
taskName |
输入 |
char* |
任务名称字符串,用于标识和调试 |
taskStart |
输入 |
void ()(void) |
任务主函数指针,任务启动后执行该函数 |
argv |
输入 |
void* |
传递给任务主函数的用户参数 |
返回值说明
QOSA_OK (0):函数执行成功
QOSA_ERROR_NO_MEMORY:内存分配失败
其他负数值:底层OSA接口返回的错误码
备注
标准版任务没有内置消息队列,适用于独立运行的任务。
任务创建后通过信号量进行同步启动,确保任务参数完全初始化。
qosa_task_create_static¶
功能描述
创建静态版任务。函数原型
int qosa_task_create_static(qosa_task_t* taskRef,
void* stackMem,
qosa_uint32_t stackSize,
void* tcbMem,
qosa_uint32_t tcbSize,
qosa_uint8_t priority,
char* taskName,
void (*taskStart)(void*),
void* argv,
...)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
taskRef |
输出 |
qosa_task_t* |
指向任务句柄指针的地址,用于接收创建的任务句柄 |
stackMem |
输入 |
void* |
用户提供的栈内存基地址。必须为非空且在任务生命周期内有效 |
stackSize |
输入 |
qosa_uint32_t |
任务堆栈大小。单位:字节 |
tcbMem |
输入 |
void* |
用户提供的TCB内存基地址 |
tcbSize |
输入 |
qosa_uint32_t |
TCB内存大小。单位:字节 |
priority |
输入 |
qosa_uint8_t |
任务优先级,数值越大优先级越高 |
taskName |
输入 |
char* |
任务名称字符串,用于标识和调试 |
taskStart |
输入 |
void ()(void) |
任务主函数指针,任务启动后执行该函数 |
argv |
输入 |
void* |
传递给任务主函数的用户参数 |
返回值说明
QOSA_OK (0):函数执行成功
负数值:函数执行失败(如内存不足、参数无效等底层OSA接口错误码)
qosa_task_delete¶
功能描述
删除指定任务,并释放其占用的相关资源。函数原型
int qosa_task_delete(qosa_task_t taskRef)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
taskRef |
输入 |
qosa_task_t |
任务句柄 |
返回值说明
QOSA_OK (0):函数执行成功
QOSA_ERROR_PARAM_IS_NULL:输入参数为空
其他负数值:底层OSA接口返回的错误码
备注
任务删除分为两种模式:
自删除:任务删除自身时,采用异步方式销毁任务,避免资源访问冲突。
他删除:其他任务删除该任务时,采用同步方式直接执行删除操作。删除后的任务句柄不可再被使用。
qosa_task_suspend¶
功能描述
挂起指定任务。挂起的任务不会消耗CPU时间。函数原型
int qosa_task_suspend(qosa_task_t taskRef)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
taskRef |
输入 |
qosa_task_t |
任务句柄 |
返回值说明
QOSA_OK (0):函数执行成功
QOSA_ERROR_PARAM_IS_NULL:输入参数为空
备注
挂起的任务可通过调用 qosa_task_resume() 恢复执行。
支持对同一任务进行多次挂起操作,需要执行相同次数的恢复操作才能使其重新运行。
当高优先级任务被挂起时,低优先级任务将获得CPU使用权。
qosa_task_resume¶
功能描述
恢复被挂起的任务,使其重新进入就绪状态。函数原型
int qosa_task_resume(qosa_task_t taskRef)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
taskRef |
输入 |
qosa_task_t |
任务句柄 |
返回值说明
QOSA_OK (0):函数执行成功
QOSA_ERROR_PARAM_IS_NULL:输入参数为空
备注
仅能恢复通过调用 qosa_task_suspend() 挂起的任务。
对于被多次挂起的任务,需要执行相同次数的恢复操作才能使其重新运行。
任务恢复后,将根据其优先级重新参与系统调度。
qosa_task_get_current_ref¶
功能描述
获取当前任务句柄。函数原型
int qosa_task_get_current_ref(qosa_task_t* taskRef)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
taskRef |
输出 |
qosa_task_t* |
指向任务句柄指针的地址,用于接收当前任务句柄 |
返回值说明
QOSA_OK (0):函数执行成功
QOSA_ERROR_PARAM_IS_NULL:输入参数为空
QOSA_ERROR_GENERAL:函数执行失败
qosa_task_sleep_ms¶
功能描述
设置任务休眠时间(单位:毫秒)。函数原型
void qosa_task_sleep_ms(qosa_uint32_t ms)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
ms |
输入 |
qosa_uint32_t |
休眠时间。单位:毫秒 |
返回值说明
无
备注
任务在休眠期间处于阻塞状态,不再占用CPU资源。
实际的休眠时长可能受系统Tick精度的影响。
qosa_task_sleep_sec¶
功能描述
设置任务休眠时间(单位:秒)。函数原型
void qosa_task_sleep_sec(qosa_uint32_t s)
参数说明
参数名 |
输入/输出 |
类型 |
说明 |
|---|---|---|---|
s |
输入 |
qosa_uint32_t |
休眠时间。单位:秒 |
返回值说明
无
备注
该函数内部会将秒数转换为系统Tick数,然后执行休眠操作。
适用于较长时间的延时场景。
请避免任务长时间阻塞,以免影响系统的整体实时响应。
枚举定义¶
qosa_errcode_os_e¶
结果码枚举定义如下:
typedef enum
{
QOSA_ERROR_OK = 0,
...
QOSA_ERROR_TASK_INVALID_ERR
QOSA_ERROR_TASK_CREATE_ERR
QOSA_ERROR_TASK_SUSPEND_ERR
QOSA_ERROR_TASK_RESUME_ERR
...
}qosa_errcode_os_e;
成员 |
说明 |
|---|---|
QOSA_ERROR_TASK_INVALID_ERR |
任务句柄无效 |
QOSA_ERROR_TASK_CREATE_ERR |
任务创建失败 |
QOSA_ERROR_TASK_SUSPEND_ERR |
任务挂起失败 |
QOSA_ERROR_TASK_RESUME_ERR |
任务恢复失败 |
QOSA_ERROR_OK |
函数执行成功 |
应用逻辑流程图¶
示例代码¶
完整示例代码请查看 https://github.com/UniRTOS/UniRTOS-Doc-Examples/blob/main/system/thread/thread_demo.c 。