资讯动态

Zephyr应用:10-Thread

发布时间:2026/9/29 7:29:58 来源:尧图企业网站定制
摘要本文是 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|----------------三、创建线程最常用APIk_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 程序例如#includelt;zephyr/kernel.hgt;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 Priority2Thread2 Priority8为了更直观地理解多线程调度的价值下面从五个维度对比裸机顺序执行与 RTOS 多线程调度的差异对比维度裸机顺序执行RTOS 多线程调度执行方式单任务串行while (1)循环内按顺序轮询各任务多线程并发由调度器按优先级分配 CPUCPU 利用率较低某任务阻塞时 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()休眠时间到等待信号量/队列条件满足线程退出ReadyRunningSleepingPending图中展示了线程的主要状态流转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.mdprj.confCONFIG_PRINTKy CONFIG_SERIALy CONFIG_CONSOLEysrc/main.c#includelt;zephyr/kernel.hgt;#includelt;zephyr/sys/printk.hgt;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()输出被缓冲串口终端刷新不及时。解决步骤检查K_THREAD_DEFINE()的参数顺序确认入口函数名与定义一致K_THREAD_DEFINE(thread1_id,/* 线程 ID */1024,/* 栈大小 */thread1,/* 入口函数 */NULL,/* 参数 1 */NULL,/* 参数 2 */NULL,/* 参数 3 */5,/* 优先级 */0,/* 选项 */0/* 延时 */);确认入口函数内部有循环和休眠否则线程执行一次就退出了voidthread1(void){while(1){printk(Thread1\n);k_sleep(K_MSEC(1000));}}在prj.conf中确认已开启串口打印CONFIG_PRINTKy CONFIG_SERIALy CONFIG_CONSOLEy如果仍然没有输出尝试把栈大小从1024增大到2048排除栈溢出导致的崩溃。问题 2优先级设置无效现象明明给 Thread1 设置了更高的优先级数值更小但两个线程的输出频率看起来差不多甚至低优先级线程反而更频繁。可能原因两个线程都在k_sleep()后进入 Sleeping休眠时间不同导致输出频率不同与优先级无关。高优先级线程休眠时间较长低优先级线程休眠时间较短低优先级线程反而更频繁获得 CPU。没有启用时间片轮转同优先级线程之间不会自动切换。解决步骤先确认两个线程的休眠时间是否一致。如果 Thread1 休眠 1000ms、Thread2 休眠 100ms那么 Thread2 输出更频繁是正常的与优先级无关。验证优先级是否生效可以让高优先级线程不休眠地持续打印观察它是否独占 CPUvoidhigh_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最高 */如果希望同优先级线程之间也能轮流执行需要在prj.conf中启用时间片轮转CONFIG_TIMESLICINGy问题 3栈溢出导致系统崩溃现象程序运行一段时间后突然停止输出或者系统复位重启串口出现异常。可能原因线程栈大小设置过小局部变量、函数调用栈把栈空间耗尽。在中断或线程中使用了较大的局部数组。递归调用或深层函数调用导致栈增长过快。解决步骤增大栈大小例如从1024改为2048或4096K_THREAD_DEFINE(thread1_id,2048,thread1,NULL,NULL,NULL,5,0,0);避免在线程内定义过大的局部数组改用静态或全局变量/* 不推荐局部大数组占用栈空间 */voidthread1(void){charbuffer[1024];/* 占用 1KB 栈 */while(1){/* ... */}}/* 推荐使用静态数组不占栈空间 */staticcharbuffer[1024];voidthread1(void){while(1){/* ... */}}在prj.conf中开启栈溢出检测方便定位问题CONFIG_THREAD_STACK_INFOy CONFIG_DEBUG_THREAD_INFOy开启后如果发生栈溢出系统会打印相关错误信息帮助你定位是哪个线程出了问题。问题 4线程只运行一次就退出现象线程打印一次后就不再输出主线程仍在运行。可能原因线程入口函数没有while (1)循环执行完就返回了。线程入口函数返回后线程进入 Dead 状态不会再被调度。解决步骤确保线程入口函数内部有无限循环voidthread1(void){while(1){printk(Thread1\n);k_sleep(K_MSEC(1000));}}如果确实需要线程只执行一次后退出这是正常行为无需修复但要注意线程退出后其栈空间仍然被占用。本课重点总结完成本课后你应该掌握K_THREAD_DEFINE() 创建静态线程k_sleep() / k_msleep() 让线程主动让出 CPUZephyr 线程优先级规则数值越小优先级越高线程的基本生命周期Ready → Running → Sleeping → Ready多线程并发执行与调度的基本概念

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑