一、嵌入式设备并发任务的痛点:为什么多线程容易“卡壳”
很多做嵌入式开发的朋友都有过这样的经历:设备明明加了多线程,本来想让几个任务同时跑起来提升速度,结果反而比单线程还卡——比如智能门锁的指纹识别、密码验证、蓝牙解锁三个线程同时跑,结果要么解锁延迟一两秒,要么指纹识别出错;或者智能温感设备的采集、计算、上报三个线程,经常出现上报数据延迟、甚至丢数据的情况。
其实嵌入式设备和电脑不一样,它的硬件资源特别“抠门”:CPU可能是只有几百MHz的小芯片,内存才几MB甚至几十MB,不像电脑有几核几GHz的CPU、几十GB的内存可以挥霍。所以电脑上随便写的多线程代码,放到嵌入式设备上就容易踩坑。
1.1 嵌入式多线程的核心痛点拆解
具体来说,常见的问题有三个: 第一个是“抢资源”,比如多个线程同时要读同一块传感器数据、或者同时往同一个串口发数据,要是没协调好,就会导致数据混乱; 第二个是“CPU占满”,比如某个线程里有个死循环的采集任务,没加合理的等待机制,把CPU占得死死的,其他线程根本没机会跑; 第三个是“线程太多”,嵌入式设备的CPU可能只有1核,你开了十几个线程,系统光在切换线程上就要花大量时间,反而得不偿失。
二、嵌入式Linux多线程的基础逻辑:用示例讲透核心
要突破性能瓶颈,得先把基础逻辑搞明白。这里我们统一用C语言+嵌入式Linux的pthread库来写示例,所有示例都基于这个技术栈,不会乱换。
2.1 最基础的多线程示例:先看“正常”的写法
先写一个简单的多线程程序,模拟两个任务:一个是采集温感数据的任务,一个是把数据上报到云的任务。
// 技术栈:C语言 + 嵌入式Linux pthread库
#include <stdio.h>
#include <pthread.h>
#include <unistd.h> // 用于sleep函数
// 定义一个全局变量,用来存储采集到的温感数据
int temp_data = 0;
// 采集温感数据的线程函数
void *collect_temp(void *arg) {
while (1) { // 循环采集
// 模拟采集:每1秒采集一次,数值从10开始递增
temp_data += 1;
printf("采集到温感数据:%d\n", temp_data);
sleep(1); // 休眠1秒,让出CPU
}
return NULL;
}
// 上报数据的线程函数
void *report_temp(void *arg) {
while (1) { // 循环上报
printf("上报温感数据:%d\n", temp_data);
sleep(2); // 每2秒上报一次
}
return NULL;
}
int main() {
pthread_t collect_thread, report_thread; // 定义两个线程的标识符
// 创建采集线程
if (pthread_create(&collect_thread, NULL, collect_temp, NULL) != 0) {
perror("创建采集线程失败");
return 1;
}
// 创建上报线程
if (pthread_create(&report_thread, NULL, report_temp, NULL) != 0) {
perror("创建上报线程失败");
return 1;
}
// 让主线程等待两个子线程结束(实际嵌入式设备中主线程可能不退出,这里只是示例逻辑)
pthread_join(collect_thread, NULL);
pthread_join(report_thread, NULL);
return 0;
}
这个代码看起来没问题,但放到嵌入式设备上跑,很快就会出问题:比如有时候上报的数据和采集的数据对不上,甚至出现上报的数据比采集的还小的情况。
2.2 问题根源:共享资源的“脏读”
上面的问题是因为两个线程共享了temp_data这个全局变量。比如采集线程刚把temp_data改成11,还没来得及打印,上报线程就把这个11读走了,打印成“上报温感数据:11”,这时候采集线程又把temp_data改成12,结果就会出现逻辑混乱。
这种多个线程同时读写同一个共享资源的情况,在嵌入式Linux里叫“临界区”,要是不加保护,就会出“脏读”“脏写”的问题。
三、突破技巧一:用互斥锁保护共享资源,避免“抢资源”
解决临界区问题的常用方法是用互斥锁(pthread_mutex),相当于给共享资源加了一把锁:一个线程进去操作的时候,把锁关上,其他线程要进去必须等锁打开。
3.1 互斥锁的完整示例
我们把上面的代码改一下,给temp_data加互斥锁:
// 技术栈:C语言 + 嵌入式Linux pthread库
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int temp_data = 0;
// 定义互斥锁
pthread_mutex_t temp_mutex;
void *collect_temp(void *arg) {
while (1) {
// 加锁:只有拿到锁才能进入临界区
pthread_mutex_lock(&temp_mutex);
temp_data += 1;
printf("采集到温感数据:%d\n", temp_data);
// 解锁:操作完必须解锁,不然其他线程永远拿不到锁
pthread_mutex_unlock(&temp_mutex);
sleep(1);
}
return NULL;
}
void *report_temp(void *arg) {
while (1) {
// 加锁
pthread_mutex_lock(&temp_mutex);
printf("上报温感数据:%d\n", temp_data);
// 解锁
pthread_mutex_unlock(&temp_mutex);
sleep(2);
}
return NULL;
}
int main() {
// 初始化互斥锁
pthread_mutex_init(&temp_mutex, NULL);
pthread_t collect_thread, report_thread;
if (pthread_create(&collect_thread, NULL, collect_temp, NULL) != 0) {
perror("创建采集线程失败");
return 1;
}
if (pthread_create(&report_thread, NULL, report_temp, NULL) != 0) {
perror("创建上报线程失败");
return 1;
}
pthread_join(collect_thread, NULL);
pthread_join(report_thread, NULL);
// 销毁互斥锁(实际嵌入式设备中主线程不退出的话,可能不需要销毁,这里只是规范)
pthread_mutex_destroy(&temp_mutex);
return 0;
}
加了互斥锁之后,就不会出现脏读的问题了。但要注意:互斥锁不能乱用,要是一个线程加了锁之后,没解锁就退出了,那整个程序就会卡死。
四、突破技巧二:合理控制线程数量,避免“线程切换开销”
很多人觉得线程越多,并发能力越强,其实在嵌入式设备上,尤其是单核CPU的设备,线程数量太多反而会拖慢速度。因为系统切换线程的时候,需要保存当前线程的运行状态(比如CPU寄存器的值、内存的位置),再加载下一个线程的状态,这个过程是有开销的。
4.1 线程数量的最优控制方法
嵌入式设备的CPU如果是1核,线程数量最好控制在“核心数+1”以内,比如1核CPU最多开2-3个线程;如果是2核CPU,最多开3-4个线程。要是任务太多,可以用“线程池”的思路,把任务排队,用固定数量的线程来处理,避免线程数量无限制增加。
4.2 线程池的简化示例
比如我们有5个上报任务,不想开5个线程,就用一个线程池,固定2个线程来处理:
// 技术栈:C语言 + 嵌入式Linux pthread库
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
// 定义任务结构体:用来存储任务的编号
typedef struct {
int task_id;
} Task;
// 任务队列:用一个数组模拟,最多存10个任务
Task task_queue[10];
int queue_size = 0; // 队列当前的任务数量
// 互斥锁:保护任务队列
pthread_mutex_t queue_mutex;
// 条件变量:用来通知线程有新任务
pthread_cond_t queue_cond;
// 工作线程函数:从队列里取任务处理
void *worker_thread(void *arg) {
int thread_id = *(int*)arg;
free(arg); // 释放传进来的线程编号内存
while (1) {
// 加锁,准备取任务
pthread_mutex_lock(&queue_mutex);
// 要是队列是空的,就等待条件变量被触发(有新任务)
while (queue_size == 0) {
pthread_cond_wait(&queue_cond, &queue_mutex);
}
// 取队列的第一个任务
Task task = task_queue[0];
// 把队列里的任务往前移一位,模拟队列的先进先出
for (int i = 0; i < queue_size - 1; i++) {
task_queue[i] = task_queue[i + 1];
}
queue_size--;
// 解锁队列
pthread_mutex_unlock(&queue_mutex);
// 处理任务:模拟上报,每个任务处理1秒
printf("线程%d处理任务%d\n", thread_id, task.task_id);
sleep(1);
}
return NULL;
}
// 添加任务到队列的函数
void add_task(int task_id) {
pthread_mutex_lock(&queue_mutex);
task_queue[queue_size].task_id = task_id;
queue_size++;
// 通知所有等待的线程:有新任务了
pthread_cond_broadcast(&queue_cond);
pthread_mutex_unlock(&queue_mutex);
}
int main() {
// 初始化互斥锁和条件变量
pthread_mutex_init(&queue_mutex, NULL);
pthread_cond_init(&queue_cond, NULL);
// 创建2个工作线程(固定数量)
for (int i = 0; i < 2; i++) {
int *thread_id = malloc(sizeof(int));
*thread_id = i;
pthread_t thread;
if (pthread_create(&thread, NULL, worker_thread, thread_id) != 0) {
perror("创建工作线程失败");
return 1;
}
}
// 模拟添加5个任务
for (int i = 1; i <= 5; i++) {
add_task(i);
printf("添加任务%d\n", i);
sleep(0.5); // 每0.5秒添加一个任务
}
// 让主线程等待,避免程序退出
while (1) {
sleep(100);
}
return 0;
}
这个线程池的代码,不管有多少个任务,都只用2个线程来处理,不会因为任务太多导致线程切换开销过大。
五、突破技巧三:用线程优先级调度,给重要任务“开绿灯”
嵌入式设备上的任务有轻重缓急,比如智能门锁的解锁任务,肯定比后台的日志上报任务重要。要是普通任务占了CPU,重要任务就没法及时跑,这时候就需要用线程优先级调度,给重要任务更高的优先级,让CPU优先处理它。
5.1 线程优先级调度的示例
嵌入式Linux的线程调度有两种策略:一种是普通的SCHED_OTHER(时间片轮转),一种是实时调度SCHED_FIFO(先到先得)。实时调度的优先级比普通调度高,适合重要任务。
// 技术栈:C语言 + 嵌入式Linux pthread库
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sched.h> // 用于设置调度策略和优先级
// 高优先级任务:模拟解锁验证
void *high_priority_task(void *arg) {
while (1) {
printf("执行高优先级任务:解锁验证\n");
sleep(1);
}
return NULL;
}
// 低优先级任务:模拟日志上报
void *low_priority_task(void *arg) {
while (1) {
printf("执行低优先级任务:日志上报\n");
sleep(1);
}
return NULL;
}
int main() {
pthread_t high_thread, low_thread;
// 设置高优先级线程的属性
pthread_attr_t high_attr;
pthread_attr_init(&high_attr);
// 设置调度策略为实时SCHED_FIFO
pthread_attr_setschedpolicy(&high_attr, SCHED_FIFO);
// 设置优先级:实时调度的优先级范围是1-99,数字越大优先级越高
struct sched_param high_param;
high_param.sched_priority = 90; // 高优先级
pthread_attr_setschedparam(&high_attr, &high_param);
// 创建高优先级线程
if (pthread_create(&high_thread, &high_attr, high_priority_task, NULL) != 0) {
perror("创建高优先级线程失败");
return 1;
}
// 设置低优先级线程的属性
pthread_attr_t low_attr;
pthread_attr_init(&low_attr);
pthread_attr_setschedpolicy(&low_attr, SCHED_FIFO);
struct sched_param low_param;
low_param.sched_priority = 10; // 低优先级
pthread_attr_setschedparam(&low_attr, &low_param);
// 创建低优先级线程
if (pthread_create(&low_thread, &low_attr, low_priority_task, NULL) != 0) {
perror("创建低优先级线程失败");
return 1;
}
pthread_join(high_thread, NULL);
pthread_join(low_thread, NULL);
return 0;
}
这个代码跑起来之后,你会发现高优先级的“解锁验证”任务会比低优先级的“日志上报”任务更频繁地执行,CPU会优先给高优先级的任务分配时间。
六、应用场景、技术优缺点、注意事项
6.1 应用场景
嵌入式Linux多线程的应用场景非常广,只要是需要同时处理多个任务的嵌入式设备都能用:比如智能门锁(解锁验证、蓝牙连接、日志上报)、智能温感设备(数据采集、计算、上报)、工业控制设备(传感器采集、设备控制、数据传输)、智能家居网关(多个子设备的通信、数据转发)等。
6.2 技术优缺点
优点:
- 比单线程更灵活,能同时处理多个任务,提升设备的响应速度;
- 比多进程更省资源,线程之间共享内存,不需要额外的内存开销;
- 可以通过优先级调度,给重要任务优先处理的权限。
缺点:
- 调试难度比单线程大,多线程的问题(比如死锁、脏读)很难复现和定位;
- 对硬件资源的要求比单线程高,要是资源不足,反而会拖慢速度;
- 要是线程设计不合理,容易出现死锁(比如两个线程互相等待对方的锁)。
6.3 注意事项
- 互斥锁一定要成对使用,加锁之后必须解锁,不然会导致死锁;
- 线程数量不能太多,嵌入式设备的CPU核心数少,线程太多会导致切换开销过大;
- 共享资源一定要加锁保护,不然会出现脏读、脏写的问题;
- 实时调度的优先级不能随便设太高,要是某个高优先级的线程一直占着CPU,其他线程就没机会跑了;
- 多线程程序一定要做充分的测试,尤其是资源竞争的场景。
七、文章总结
嵌入式Linux多线程编程的性能瓶颈,本质上是因为嵌入式设备的硬件资源有限,而多线程的设计没有适配这些资源。要突破这些瓶颈,核心是三个技巧:一是用互斥锁保护共享资源,避免抢资源;二是合理控制线程数量,避免线程切换开销;三是用优先级调度,给重要任务开绿灯。
这些技巧的核心逻辑都是围绕“适配嵌入式设备的有限资源”展开的,只要掌握了这些逻辑,就能写出适合嵌入式设备的多线程程序,让设备的并发能力得到真正的提升。
评论
围绕“嵌入式Linux多线程编程:嵌入式设备并发任务调度的性能瓶颈突破技巧”参与讨论