news 2026/9/29 7:29:50

Zephyr应用:10-Thread

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Zephyr应用:10-Thread

摘要:本文是 Zephyr RTOS 多线程编程的入门实战课。你将理解线程(Thread)与裸机顺序执行的区别,掌握K_THREAD_DEFINE()创建静态线程、k_sleep()/k_msleep()主动让出 CPU、线程优先级(数值越小优先级越高)以及线程生命周期(Ready → Running → Sleeping → Ready)等核心概念,并通过三个线程交替打印的完整实验,在 ESP32-S3 或 NUCLEO-F303RE 上观察多线程并发调度的实际效果。

10 - Thread(线程)

学习目标:

理解 Zephyr RTOS 的多线程机制,学会创建线程、设置优先级、线程休眠以及线程调度。

这是从“写程序”迈向“RTOS”最重要的一课。

一、Thread 是什么

在裸机程序中,一般都是

while(1){task1();task2();task3();}

CPU 只能按顺序执行。
例如

采集ADC ↓ 发送UART ↓ 刷新LED ↓ 再次采集ADC

所有事情都是串行。
在 RTOS 中则变成

Thread A 采集ADC Thread B UART发送 Thread C LED闪烁

CPU 负责不断切换。
例如

CPU ↓ Thread A ↓ Thread B ↓ Thread C ↓ Thread A ↓ Thread B

这就是 Thread。

二、Zephyr 线程模型

在 Zephyr 中,每个线程都有:

入口函数 Stack Priority 状态

例如

Thread1voidthread1(void){}Thread2voidthread2(void){}

每个线程都有自己的 Stack。

+----------------+|Thread1 Stack|+----------------+ +----------------+|Thread2 Stack|+----------------+ +----------------+|Main Stack|+----------------+

三、创建线程
最常用API

k_thread_create()

但是官方更推荐

K_THREAD_DEFINE()

因为简单。

例如

K_THREAD_DEFINE(blink_tid,1024,blink_thread,NULL,NULL,NULL,5,0,0);

参数分别表示:

线程ID Stack大小 入口函数 参数1 参数2 参数3 Priority Options Delay 线程入口
voidblink_thread(void){while(1){}}

四、第一个 Thread 程序

例如:

#include<zephyr/kernel.h>voidthread1(void){while(1){printk("Thread1\\n");k_sleep(K_MSEC(1000));}}K_THREAD_DEFINE(thread1_id,1024,thread1,NULL,NULL,NULL,5,0,0);intmain(void){while(1){printk("Main\\n");k_sleep(K_MSEC(500));}}

输出类似:

Main Main Thread1 Main Main Thread1 Main Thread1

说明:
Main线程和Thread1同时运行。

五、两个线程
增加第二个线程:

voidthread2(void){while(1){printk("Thread2\\n");k_sleep(K_MSEC(700));}}

再定义

K_THREAD_DEFINE(thread2_id,1024,thread2,NULL,NULL,NULL,5,0,0);

运行后可能看到:

Main Thread1 Thread2 Main Thread2 Main Thread1 Main Thread2

三个线程共同运行。

六、线程优先级
Zephyr中:

数字越小 优先级越高

例如:

Priority0最高
Priority1↓ Priority2↓ Priority5↓ Priority10↓ 最低

例如:

Thread1 Priority=2Thread2 Priority=8

为了更直观地理解多线程调度的价值,下面从五个维度对比裸机顺序执行与 RTOS 多线程调度的差异:

对比维度裸机顺序执行RTOS 多线程调度
执行方式单任务串行,while (1)循环内按顺序轮询各任务多线程并发,由调度器按优先级分配 CPU
CPU 利用率较低,某任务阻塞时 CPU 空转等待较高,某线程休眠/等待时 CPU 立即切换给其他线程
实时性较差,高优先级任务可能被前面的任务阻塞较好,高优先级线程可抢占低优先级线程
代码复杂度简单,逻辑线性直观较高,需考虑线程同步、资源共享与优先级设计
适用场景功能简单、任务少、实时性要求低的场景任务多、实时性要求高、需要并发处理的场景

说明:裸机程序结构简单、易于调试,但任务之间相互阻塞,CPU 利用率低;RTOS 通过线程调度让 CPU 在多个任务间高效切换,显著提升实时性与资源利用率,代价是代码结构和调试难度更高。实际项目中应根据需求权衡选择。

那么Thread1会更容易获得CPU。

为了更直观地理解,下面列出不同优先级数值对应的调度优先级和典型使用场景:

优先级数值调度优先级典型使用场景
0最高实时性要求极高的任务,如电机控制、紧急中断处理
1很高关键控制任务,如传感器数据采集、通信协议处理
5中等普通业务逻辑,如状态机、数据处理
10较低后台任务,如日志输出、统计信息汇总

提示:数值越小,优先级越高。优先级 0 的线程会优先于优先级 5 的线程获得 CPU;实际项目中建议为不同实时性需求的任务分配合理的优先级,避免高优先级线程长期占用 CPU 导致低优先级线程饿死。

七、线程休眠
最常用:

k_sleep(K_SECONDS(1));

或者

k_sleep(K_MSEC(500));

也可以

k_msleep(500);

线程休眠期间:

CPU可以运行其它线程。

如果线程一直运行而不休眠:

while(1){}

它可能持续占用CPU,导致其他同优先级线程得不到执行机会(除非启用了时间片轮转)。

八、线程状态
线程一般有:

Ready Running Sleeping Pending Dead

流程可以用下面的状态图更直观地表示:

调度器选中

时间片用完/被抢占

k_sleep()

休眠时间到

等待信号量/队列

条件满足

线程退出

Ready

Running

Sleeping

Pending

图中展示了线程的主要状态流转:

  • Ready(就绪):线程已准备好运行,等待调度器分配 CPU。
  • Running(运行):线程正在占用 CPU 执行代码。
  • Sleeping(休眠):线程调用k_sleep()主动让出 CPU,休眠结束后回到 Ready。
  • Pending(挂起):线程等待某个事件(如信号量、消息队列),条件满足后回到 Ready。
  • Dead(终止):线程执行完毕退出。

九、实验
创建三个线程。

Main 每500ms打印一次
Thread A 每1000ms打印一次
Thread B 每1500ms打印一次

预期输出:

Main ThreadA Main ThreadB Main ThreadA Main ThreadB

注意:由于线程调度和 printk() 的执行时机不同,每次输出顺序可能略有变化,这是正常现象。

十、推荐目录结构

10-Thread/│ ├── CMakeLists.txt ├── prj.conf ├── src/│ └── main.c └── README.md

prj.conf

CONFIG_PRINTK=y CONFIG_SERIAL=y CONFIG_CONSOLE=y

src/main.c

#include<zephyr/kernel.h>#include<zephyr/sys/printk.h>voidthread_a(void){while(1){printk("Thread A\\n");k_sleep(K_SECONDS(1));}}voidthread_b(void){while(1){printk("Thread B\\n");k_sleep(K_MSEC(1500));}}K_THREAD_DEFINE(thread_a_id,1024,thread_a,NULL,NULL,NULL,5,0,0);K_THREAD_DEFINE(thread_b_id,1024,thread_b,NULL,NULL,NULL,5,0,0);intmain(void){while(1){printk("Main\\n");k_sleep(K_MSEC(500));}}

编译并下载到你的ESP32-S3或NUCLEO-F303RE后,就能在串口终端观察三个线程交替输出。编译并下载到你的ESP32-S3或NUCLEO-F303RE后,就能在串口终端观察三个线程交替输出。

十一、常见问题与排查

在实际运行中,你可能会遇到下面几种典型问题。这里给出原因分析和解决步骤,帮助你快速定位。

问题 1:线程不打印

现象:程序编译下载后,串口终端只有 Main 输出,看不到 Thread1 / Thread2 的输出。

可能原因:

  • 线程入口函数没有被正确调用(K_THREAD_DEFINE()参数顺序写错)。
  • 线程栈大小设置过小,导致线程启动时栈溢出而崩溃。
  • 线程优先级设置过高或过低,导致调度器一直不选中它。
  • printk()输出被缓冲,串口终端刷新不及时。

解决步骤:

  1. 检查K_THREAD_DEFINE()的参数顺序,确认入口函数名与定义一致:
K_THREAD_DEFINE(thread1_id,/* 线程 ID */1024,/* 栈大小 */thread1,/* 入口函数 */NULL,/* 参数 1 */NULL,/* 参数 2 */NULL,/* 参数 3 */5,/* 优先级 */0,/* 选项 */0/* 延时 */);
  1. 确认入口函数内部有循环和休眠,否则线程执行一次就退出了:
voidthread1(void){while(1){printk("Thread1\n");k_sleep(K_MSEC(1000));}}
  1. 在prj.conf中确认已开启串口打印:
CONFIG_PRINTK=y CONFIG_SERIAL=y CONFIG_CONSOLE=y
  1. 如果仍然没有输出,尝试把栈大小从1024增大到2048,排除栈溢出导致的崩溃。

问题 2:优先级设置无效

现象:明明给 Thread1 设置了更高的优先级(数值更小),但两个线程的输出频率看起来差不多,甚至低优先级线程反而更频繁。

可能原因:

  • 两个线程都在k_sleep()后进入 Sleeping,休眠时间不同导致输出频率不同,与优先级无关。
  • 高优先级线程休眠时间较长,低优先级线程休眠时间较短,低优先级线程反而更频繁获得 CPU。
  • 没有启用时间片轮转,同优先级线程之间不会自动切换。

解决步骤:

  1. 先确认两个线程的休眠时间是否一致。如果 Thread1 休眠 1000ms、Thread2 休眠 100ms,那么 Thread2 输出更频繁是正常的,与优先级无关。

  2. 验证优先级是否生效,可以让高优先级线程不休眠地持续打印,观察它是否独占 CPU:

voidhigh_prio_thread(void){while(1){printk("High\n");}}K_THREAD_DEFINE(high_prio_id,1024,high_prio_thread,NULL,NULL,NULL,0,0,0);/* 优先级 0,最高 */
  1. 如果希望同优先级线程之间也能轮流执行,需要在prj.conf中启用时间片轮转:
CONFIG_TIMESLICING=y

问题 3:栈溢出导致系统崩溃

现象:程序运行一段时间后突然停止输出,或者系统复位重启,串口出现异常。

可能原因:

  • 线程栈大小设置过小,局部变量、函数调用栈把栈空间耗尽。
  • 在中断或线程中使用了较大的局部数组。
  • 递归调用或深层函数调用导致栈增长过快。

解决步骤:

  1. 增大栈大小,例如从1024改为2048或4096:
K_THREAD_DEFINE(thread1_id,2048,thread1,NULL,NULL,NULL,5,0,0);
  1. 避免在线程内定义过大的局部数组,改用静态或全局变量:
/* 不推荐:局部大数组占用栈空间 */voidthread1(void){charbuffer[1024];/* 占用 1KB 栈 */while(1){/* ... */}}/* 推荐:使用静态数组,不占栈空间 */staticcharbuffer[1024];voidthread1(void){while(1){/* ... */}}
  1. 在prj.conf中开启栈溢出检测,方便定位问题:
CONFIG_THREAD_STACK_INFO=y CONFIG_DEBUG_THREAD_INFO=y

开启后,如果发生栈溢出,系统会打印相关错误信息,帮助你定位是哪个线程出了问题。

问题 4:线程只运行一次就退出

现象:线程打印一次后就不再输出,主线程仍在运行。

可能原因:

  • 线程入口函数没有while (1)循环,执行完就返回了。
  • 线程入口函数返回后,线程进入 Dead 状态,不会再被调度。

解决步骤:

  1. 确保线程入口函数内部有无限循环:
voidthread1(void){while(1){printk("Thread1\n");k_sleep(K_MSEC(1000));}}
  1. 如果确实需要线程只执行一次后退出,这是正常行为,无需修复;但要注意线程退出后其栈空间仍然被占用。

本课重点总结
完成本课后,你应该掌握:
K_THREAD_DEFINE() 创建静态线程
k_sleep() / k_msleep() 让线程主动让出 CPU
Zephyr 线程优先级规则(数值越小,优先级越高)

线程的基本生命周期(Ready → Running → Sleeping → Ready)

多线程并发执行与调度的基本概念

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 7:29:37

AI工程从零搭建:RAG、Prompt与Agent工作流实战

很多人问我,“AI工程到底怎么入门?”我自己也是从一个个零散脚本起步,踩了不少坑,才慢慢摸出一套能复用的工程化路径。今天这篇就围绕“ai-engineering-from-scratch”这个项目主题,聊聊从零搭一个AI工程的全部关键环节…

作者头像 李华
网站建设 2026/9/29 7:29:00

GPEN人脸修复:基于GAN先验的盲人脸复原技术解析

1. 先聊一聊:为什么人脸修复到了GPEN这里才算“能用”做图像修复、老照片还原或者视频增强的朋友,应该都经历过这种折磨:拿一张百年前斑驳的人像照或一张监控里的低分辨率人脸,跑传统超分算法,出来的图是“清晰了”&am…

作者头像 李华
网站建设 2026/9/29 7:28:32

Verilog高频语法速查:从reg/wire到状态机的工程清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 7:26:59

芯片设计方法演化史:从标准单元到Chiplet的五大实战拐点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 7:26:31

数据库管理工具怎么选?Navicat安装连接与排查全攻略

搞数据库的朋友应该都有这种感觉:能从命令行把SQL写利索的人不少,但从“会写SQL”到“把一套库管起来”,中间隔着一个工具链的距离。“数据库及navicat工具”这个话题最近被问得非常多,我发现大多数人卡住的根本不是SQL语法&#…

作者头像 李华
网站建设 2026/9/29 7:23:08

DeepAgents+MCP+A2A+Skills:多智能体集群实战与踩坑指南

1. 为什么我开始搭多智能体集群:单Agent的瓶颈前几天帮团队把一个内部工具站整体改版,最开始图省事,直接用单个Agent挂着十几个MCP工具从头跑到尾。结果改了首页忘了侧边栏,改完筛选逻辑回头又碰坏了登录态,Agent自己都…

作者头像 李华