嵌入式 Linux 快速入门(四)
课程摘要:本课程是一份面向嵌入式 Linux 初学者的实践导向速成指南。上一篇我们学习了进程间通信的多种方式,这一篇我们继续深入线程的核心知识。课程摒弃繁杂的理论,以动手实操为核心,带你掌握线程的创建、回收、终止、取消、分离以及互斥锁同步,为后续多线程并发项目开发打下坚实基础。
开始前的准备:跟上一篇一样,VMware + Ubuntu 虚拟机、VSCode + Remote-SSH 插件环境准备好就行,编译线程程序记得加-pthread参数哦!
学习目标:
线程:
理解什么是线程以及和进程的区别,学会线程的创建、回收、终止、取消、分离操作,掌握互斥锁的使用,理解线程同步问题
学习内容:
1. 为什么需要线程?
我们先搞懂两个问题:
什么是线程?
有了进程为什么还要线程?
上一篇我们学了进程,进程是操作系统分配资源的最小单位,每个进程都有自己独立的内存空间、代码段、数据段、堆栈和文件描述符。但进程有个问题——创建和销毁的开销比较大,切换起来也慢。
线程就是来解决这个问题的。线程是操作系统中最小的执行单位,一个进程可以包含一个或多个线程,同一个进程里的多个线程共享内存、文件描述符等资源,创建和销毁的开销非常小,切换也快得多。
简单类比一下:
- 进程就像一个工厂,有自己的厂房、设备、原材料(独立资源)
- 线程就像工厂里的工人,多个工人共用厂房和设备(共享资源),但各自干自己的活
进程和线程的区别:
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源开销 | 有独立地址空间,开销大 | 共享进程资源,开销小 |
| 通信效率 | 需要 IPC 机制(管道、消息队列等) | 直接访问共享内存,又快又简单 |
| 调度灵活性 | 一个阻塞整个进程挂起 | 一个线程阻塞,其他线程可以继续跑 |
| 安全性 | 地址空间隔离,一个崩了不影响其他 | 共享地址空间,一个线程崩了整个进程都没了 |
💡 一句话总结:进程是资源分配的最小单位,线程是执行调度的最小单位。
2. 创建线程(pthread_create)
创建线程用pthread_create函数。
函数原型:
#include<pthread.h>intpthread_create(pthread_t*thread,constpthread_attr_t*attr,void*(*start_routine)(void*),void*arg);参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| thread | pthread_t * | 输出参数,存新线程的 ID | 传一个 pthread_t 变量的地址 |
| attr | const pthread_attr_t * | 线程属性 | 一般传 NULL 用默认属性 |
| start_routine | void()(void *) | 线程入口函数指针 | 线程创建后就跑这个函数,参数和返回值都是 void* |
| arg | void * | 传给线程函数的参数 | 要传多个参数就封装成结构体 |
| 返回值 | 含义 |
|---|---|
| 0 | 成功 |
| 非 0 | 失败,返回错误码(注意:不设置 errno!) |
⚠️ 注意:编译线程相关的程序,必须加
-pthread参数!比如:gcc demo.c -o demo -pthread
代码示例
/* 头文件说明: <stdio.h> printf <pthread.h> pthread_create、pthread_t */#include<stdio.h>#include<pthread.h>/* 线程入口函数 参数:void* 类型,可以传任意类型的数据,用的时候强转 返回值:void* 类型,线程的返回值,可以被 pthread_join 拿到 */void*print_message(void*str){// 把 void* 强转回 char*,然后打印printf("%s\n",(char*)str);returnNULL;// 线程结束,返回 NULL}intmain(){pthread_tthread_id;// 线程 ID 变量char*message="hello pthread";// 要传给线程的字符串intresult;/* 创建线程 参数1:&thread_id 传出线程ID 参数2:NULL 默认属性 参数3:print_message 线程入口函数 参数4:(void*)message 传给线程的参数 */result=pthread_create(&thread_id,NULL,print_message,(void*)message);if(result!=0){printf("错误:创建线程失败\n");return-1;}printf("主线程结束\n");return0;}现在来讲讲代码的实现:
运行一下你会发现,可能只打印了"主线程结束",子线程的"hello pthread"没出来。这是为什么呢?
因为主线程执行得快,return 0之后整个进程就退出了,子线程可能还没来得及执行就被一起带走了。
那怎么解决?用pthread_join等着子线程跑完,我们接着往下看。
3. 回收线程(pthread_join)
pthread_join的作用是等待指定线程结束,然后回收它的资源,还能拿到线程的返回值。就像进程里的waitpid。
函数原型:
#include<pthread.h>intpthread_join(pthread_tthread,void**retval);参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| thread | pthread_t | 要等待的目标线程 ID | pthread_create 时拿到的 |
| retval | void ** | 输出参数,接收线程的返回值 | 不需要返回值就传 NULL |
| 返回值 | 含义 |
|---|---|
| 0 | 成功 |
| 非 0 | 失败,返回错误码 |
代码示例
#include<stdio.h>#include<pthread.h>void*print_message(void*str){printf("%s\n",(char*)str);returnNULL;}intmain(){pthread_tthread_id;char*message="hello pthread";intresult;// 创建线程result=pthread_create(&thread_id,NULL,print_message,(void*)message);if(result!=0){printf("错误:创建线程失败\n");return-1;}/* 等待子线程结束 参数1:thread_id 等哪个线程 参数2:NULL 不关心返回值 调用这个函数会阻塞,直到子线程跑完 */result=pthread_join(thread_id,NULL);if(result!=0){printf("错误:等待线程失败\n");return-1;}printf("主线程结束\n");return0;}💡 加上 pthread_join 之后再运行,就能看到子线程的打印了。主线程会一直等,等子线程跑完了才继续往下走。
4. 获取线程 ID(pthread_self)
每个线程都有自己的 ID,就像进程有 PID 一样。pthread_self()可以获取当前线程的 ID。
函数原型:
#include<pthread.h>pthread_tpthread_self(void);参数说明:无参数,直接调用就行。
| 返回值 | 含义 |
|---|---|
| pthread_t | 当前线程的 ID,永远成功不会失败 |
💡 打印线程 ID 用
%lu格式符(pthread_t 本质上是 unsigned long)。
代码示例
#include<stdio.h>#include<pthread.h>void*thread_func(void*arg){// 获取当前线程(子线程)的 IDpthread_ttid=pthread_self();printf("子线程的 ID 是 %lu\n",tid);returnNULL;}intmain(){pthread_tthread_id;intret;// 创建子线程ret=pthread_create(&thread_id,NULL,thread_func,NULL);if(ret!=0){printf("创建线程失败\n");return-1;}// 获取主线程自己的 IDpthread_tmain_tid=pthread_self();printf("主线程的 ID 是 %lu\n",main_tid);// 等子线程跑完pthread_join(thread_id,NULL);printf("主线程结束\n");return0;}5. 终止线程(pthread_exit)
线程怎么结束?有几种方式:
- 线程函数
return返回(最常用) - 调用
pthread_exit()主动退出 - 被其他线程取消(pthread_cancel)
pthread_exit就像进程里的exit,但它只终止当前线程,不影响同进程的其他线程。
函数原型:
#include<pthread.h>voidpthread_exit(void*retval);参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| retval | void * | 线程退出的返回值 | 可以被 pthread_join 的 retval 参数捕获 |
核心要点:
- 只终止当前线程,同进程的其他线程不受影响
- 主线程调用
pthread_exit,主线程退出但子线程还能继续跑(区别于exit(),exit 会终止整个进程) - 线程函数里
return等价于调用pthread_exit
代码示例
/* 头文件说明: <stdio.h> printf <stdlib.h> exit <pthread.h> pthread_create、pthread_exit、pthread_join <unistd.h> sleep */#include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<unistd.h>void*thread_func(void*arg){printf("子线程开始执行\n");sleep(3);// 模拟干活,睡 3 秒printf("子线程执行完毕\n");// 线程结束,返回一个值pthread_exit((void*)"我是子线程的返回值");}intmain(){pthread_ttid;intret;ret=pthread_create(&tid,NULL,thread_func,NULL);if(ret!=0){printf("创建线程失败\n");exit(EXIT_FAILURE);}printf("主线程即将退出(但子线程还在跑)\n");/* 主线程调用 pthread_exit 退出 注意:这时候主线程结束了,但子线程还能继续跑! 如果用 return 0 或者 exit(),整个进程就没了,子线程也跟着没了 */pthread_exit(NULL);}💡 有意思的地方:主线程
pthread_exit之后,进程不会立刻退出,会等所有线程都跑完才退出。子线程该干嘛干嘛,不受影响。
6. 取消线程(pthread_cancel)
一个线程可以给另一个线程发"取消请求",让它停下来。用pthread_cancel函数。
函数原型:
#include<pthread.h>intpthread_cancel(pthread_tthread);参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| thread | pthread_t | 要取消的目标线程 ID |
| 返回值 | 含义 |
|---|---|
| 0 | 成功(只是成功发送了取消请求) |
| 非 0 | 失败 |
注意事项:
pthread_cancel只是发送取消请求,不保证线程一定会被取消- 线程默认是可以被取消的,但它会等到某个"取消点"才真正退出(比如 sleep、read、write 这些系统调用就是取消点)
- 如果线程一直在纯计算,没有系统调用,可能收不到取消请求
代码示例
#include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<unistd.h>void*thread_function(void*arg){inti;printf("子线程开始运行\n");// 循环打印,每次 sleep 1 秒// sleep 是取消点,所以取消请求能在这里生效for(i=1;i<=5;i++){printf("子线程打印:%d\n",i);sleep(1);}printf("子线程正常结束\n");returnNULL;}intmain(){pthread_tthread;intres;// 创建子线程res=pthread_create(&thread,NULL,thread_function,NULL);if(res!=0){perror("创建线程失败");exit(EXIT_FAILURE);}// 主线程睡 2 秒,让子线程先跑一会儿sleep(2);// 给子线程发取消请求printf("主线程:取消子线程\n");if(pthread_cancel(thread)!=0){perror("取消线程失败");exit(EXIT_FAILURE);}// 还是要 join 一下,回收资源pthread_join(thread,NULL);printf("主线程结束\n");return0;}💡 运行结果:子线程打印 1、2 之后,主线程发取消请求,子线程在第 2 秒的 sleep 取消点被取消,不会继续打印 3、4、5。
7. 分离线程(pthread_detach)
默认情况下,线程结束后需要pthread_join来回收资源,不然就会变成"僵尸线程"。
但有些线程我们不需要等它结束,也不关心它的返回值,这时候就可以把它设为分离状态——线程终止时系统自动回收资源,不用 join。
函数原型:
#include<pthread.h>intpthread_detach(pthread_tthread);参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| thread | pthread_t | 要设置为分离状态的线程 ID |
| 返回值 | 含义 |
|---|---|
| 0 | 成功 |
| 非 0 | 失败 |
核心要点:
- 分离状态的线程终止后,系统自动回收资源,无需 pthread_join
- 一旦设为分离状态,就不能再转回非分离,也不能再 join 了
- 也可以在创建线程时通过属性直接设为分离状态
代码示例
#include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<unistd.h>void*thread_func(void*arg){inti;for(i=0;i<5;i++){printf("子线程打印:%d\n",i);sleep(1);}printf("子线程结束,系统自动回收资源\n");returnNULL;}intmain(){pthread_ttid;intret;// 创建线程ret=pthread_create(&tid,NULL,thread_func,NULL);if(ret!=0){printf("创建线程失败\n");exit(EXIT_FAILURE);}// 设置为分离状态ret=pthread_detach(tid);if(ret!=0){printf("设置分离状态失败\n");exit(EXIT_FAILURE);}printf("主线程:子线程已设为分离状态,不用 join\n");// 主线程睡 6 秒,等子线程跑完// 如果主线程先退出,整个进程就没了,子线程也没了sleep(6);printf("主线程结束\n");return0;}⚠️ 注意:分离的线程不能再 join 了,硬要 join 会返回 EINVAL 错误。
8. 线程同步问题
多个线程共享同一块内存,好处是通信方便,但也带来了问题——数据竞争。
举个例子:两个线程同时对一个全局变量count++,各加 10 万次,你觉得结果会是 20 万吗?
代码示例(有问题的版本)
#include<stdio.h>#include<stdlib.h>#include<pthread.h>#defineTHREAD_NUM2// 两个线程intcount=0;// 全局计数器// 每个线程都对 count 加 10 万次void*thread_func(void*arg){inti;for(i=0;i<100000;i++){count++;// 计数器加 1}pthread_exit(NULL);}intmain(){pthread_tthread_id[THREAD_NUM];intresult,i;// 创建两个线程for(i=0;i<THREAD_NUM;i++){result=pthread_create(&thread_id[i],NULL,thread_func,NULL);if(result!=0){printf("Error: pthread_create failed.\n");exit(EXIT_FAILURE);}}// 等待两个线程都结束for(i=0;i<THREAD_NUM;i++){result=pthread_join(thread_id[i],NULL);if(result!=0){printf("Error: pthread_join failed.\n");exit(EXIT_FAILURE);}}// 打印最终结果printf("最终 count = %d\n",count);pthread_exit(NULL);}现在来讲讲代码的实现:
运行几次你会发现,结果每次都不一样,而且基本都小于 200000!这是为什么呢?
因为count++看起来是一行代码,但在 CPU 层面其实是三步:
- 从内存把 count 的值读到寄存器
- 寄存器里加 1
- 把结果写回内存
如果两个线程同时执行,可能会这样:
- 线程 A 读到 count = 100
- 线程 B 也读到 count = 100
- 线程 A 加 1,写回 101
- 线程 B 加 1,写回 101
结果两次加 1,最后只加了 1 次!这就是数据竞争(Data Race)。
怎么解决?用互斥锁!
9. 互斥锁(pthread_mutex)
互斥锁(Mutex)是最常用的线程同步机制。简单说就是:访问共享资源前先加锁,访问完再解锁。同一时刻只有一个线程能拿到锁,其他线程就得等着。
互斥锁使用步骤:
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | pthread_mutex_t mutex | 定义互斥锁变量 |
| 2 | pthread_mutex_init() | 初始化互斥锁 |
| 3 | pthread_mutex_lock() | 加锁(拿不到就阻塞等) |
| 4 | 访问共享资源 | 临界区 |
| 5 | pthread_mutex_unlock() | 解锁 |
| 6 | pthread_mutex_destroy() | 销毁互斥锁 |
pthread_mutex_init 的函数原型:
#include<pthread.h>intpthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*attr);参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则与补充 |
|---|---|---|---|
| mutex | pthread_mutex_t * | 互斥锁指针 | |
| attr | const pthread_mutexattr_t * | 锁属性 | 一般传 NULL 用默认属性 |
pthread_mutex_lock 的函数原型:
intpthread_mutex_lock(pthread_mutex_t*mutex);加锁,如果锁已经被别人拿着了,就阻塞等着,直到拿到锁为止。
pthread_mutex_unlock 的函数原型:
intpthread_mutex_unlock(pthread_mutex_t*mutex);解锁,把锁还回去,等锁的线程就有机会拿到了。
pthread_mutex_destroy 的函数原型:
intpthread_mutex_destroy(pthread_mutex_t*mutex);销毁互斥锁,释放资源。
代码示例(加了互斥锁的版本)
/* 头文件说明: <stdio.h> printf <stdlib.h> exit <pthread.h> 所有线程相关函数 */#include<stdio.h>#include<stdlib.h>#include<pthread.h>#defineTHREAD_NUM2intcount=0;pthread_mutex_tmutex;// 定义互斥锁变量void*thread_func(void*arg){inti;for(i=0;i<100000;i++){/* 访问共享资源前先加锁 如果锁被别人拿着,这里就会阻塞等待 */pthread_mutex_lock(&mutex);count++;// 临界区:只有一个线程能执行这里/* 访问完解锁 */pthread_mutex_unlock(&mutex);}pthread_exit(NULL);}intmain(){pthread_tthread_id[THREAD_NUM];intresult,i;/* 初始化互斥锁 */pthread_mutex_init(&mutex,NULL);// 创建线程for(i=0;i<THREAD_NUM;i++){result=pthread_create(&thread_id[i],NULL,thread_func,NULL);if(result!=0){printf("Error: pthread_create failed.\n");exit(EXIT_FAILURE);}}// 等待线程结束for(i=0;i<THREAD_NUM;i++){result=pthread_join(thread_id[i],NULL);if(result!=0){printf("Error: pthread_join failed.\n");exit(EXIT_FAILURE);}}/* 销毁互斥锁 */pthread_mutex_destroy(&mutex);printf("最终 count = %d\n",count);pthread_exit(NULL);}现在来讲讲代码的实现:
加上互斥锁之后再运行,结果就稳稳地等于 200000 了!
原理很简单:count++这一步被锁保护起来了,同一时刻只有一个线程能进去执行,执行完了解锁,下一个线程才能进去。这样就不会出现"两个线程都读到同一个值"的情况了。
⚠️ 注意:加锁的范围要尽量小,只保护真正需要保护的共享资源,锁加太大了会影响并发性能。
以上就是 Linux 线程的基础入门了,觉得有帮助的友友们可以点赞收藏支持一下小弟!!!
学完了可以自己尝试动手完成一个小练习,步骤如下:
写一个多线程排序程序:
- 定义一个大数组(比如 10000 个元素)
- 创建两个线程,每个线程负责对数组的一半进行排序(比如冒泡排序)
- 两个线程都排好之后,主线程进行归并,把两个有序的半段合成一个完整的有序数组
- 最后打印排序后的数组验证结果
提示:两个线程操作的是不同的区域,不会互相干扰,所以不需要互斥锁。
学习产出:
- 技术笔记 1 遍
- 线程创建、回收、终止、取消、分离各写一遍代码
- 互斥锁解决数据竞争的示例 1 份
- 多线程排序综合练习 1 份
有疑问或是建议的博友们可以评论或私信我,我看到会及时解答或改正的。
