嵌入式 Linux 快速入门(四)

📅 2026/7/2 1:29:14
嵌入式 Linux 快速入门(四)
课程摘要本课程是一份面向嵌入式 Linux 初学者的实践导向速成指南。上一篇我们学习了进程间通信的多种方式这一篇我们继续深入线程的核心知识。课程摒弃繁杂的理论以动手实操为核心带你掌握线程的创建、回收、终止、取消、分离以及互斥锁同步为后续多线程并发项目开发打下坚实基础。开始前的准备跟上一篇一样VMware Ubuntu 虚拟机、VSCode Remote-SSH 插件环境准备好就行编译线程程序记得加-pthread参数哦学习目标线程理解什么是线程以及和进程的区别学会线程的创建、回收、终止、取消、分离操作掌握互斥锁的使用理解线程同步问题学习内容1. 为什么需要线程我们先搞懂两个问题什么是线程有了进程为什么还要线程上一篇我们学了进程进程是操作系统分配资源的最小单位每个进程都有自己独立的内存空间、代码段、数据段、堆栈和文件描述符。但进程有个问题——创建和销毁的开销比较大切换起来也慢。线程就是来解决这个问题的。线程是操作系统中最小的执行单位一个进程可以包含一个或多个线程同一个进程里的多个线程共享内存、文件描述符等资源创建和销毁的开销非常小切换也快得多。简单类比一下进程就像一个工厂有自己的厂房、设备、原材料独立资源线程就像工厂里的工人多个工人共用厂房和设备共享资源但各自干自己的活进程和线程的区别对比项进程线程资源开销有独立地址空间开销大共享进程资源开销小通信效率需要 IPC 机制管道、消息队列等直接访问共享内存又快又简单调度灵活性一个阻塞整个进程挂起一个线程阻塞其他线程可以继续跑安全性地址空间隔离一个崩了不影响其他共享地址空间一个线程崩了整个进程都没了 一句话总结进程是资源分配的最小单位线程是执行调度的最小单位。2. 创建线程pthread_create创建线程用pthread_create函数。函数原型#includepthread.hintpthread_create(pthread_t*thread,constpthread_attr_t*attr,void*(*start_routine)(void*),void*arg);参数说明参数数据类型详细说明使用规则与补充threadpthread_t *输出参数存新线程的 ID传一个 pthread_t 变量的地址attrconst pthread_attr_t *线程属性一般传 NULL 用默认属性start_routinevoid()(void *)线程入口函数指针线程创建后就跑这个函数参数和返回值都是 void*argvoid *传给线程函数的参数要传多个参数就封装成结构体返回值含义0成功非 0失败返回错误码注意不设置 errno⚠️ 注意编译线程相关的程序必须加-pthread参数比如gcc demo.c -o demo -pthread代码示例/* 头文件说明 stdio.h printf pthread.h pthread_create、pthread_t */#includestdio.h#includepthread.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*messagehello pthread;// 要传给线程的字符串intresult;/* 创建线程 参数1thread_id 传出线程ID 参数2NULL 默认属性 参数3print_message 线程入口函数 参数4(void*)message 传给线程的参数 */resultpthread_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_joinpthread_join的作用是等待指定线程结束然后回收它的资源还能拿到线程的返回值。就像进程里的waitpid。函数原型#includepthread.hintpthread_join(pthread_tthread,void**retval);参数说明参数数据类型详细说明使用规则与补充threadpthread_t要等待的目标线程 IDpthread_create 时拿到的retvalvoid **输出参数接收线程的返回值不需要返回值就传 NULL返回值含义0成功非 0失败返回错误码代码示例#includestdio.h#includepthread.hvoid*print_message(void*str){printf(%s\n,(char*)str);returnNULL;}intmain(){pthread_tthread_id;char*messagehello pthread;intresult;// 创建线程resultpthread_create(thread_id,NULL,print_message,(void*)message);if(result!0){printf(错误创建线程失败\n);return-1;}/* 等待子线程结束 参数1thread_id 等哪个线程 参数2NULL 不关心返回值 调用这个函数会阻塞直到子线程跑完 */resultpthread_join(thread_id,NULL);if(result!0){printf(错误等待线程失败\n);return-1;}printf(主线程结束\n);return0;} 加上 pthread_join 之后再运行就能看到子线程的打印了。主线程会一直等等子线程跑完了才继续往下走。4. 获取线程 IDpthread_self每个线程都有自己的 ID就像进程有 PID 一样。pthread_self()可以获取当前线程的 ID。函数原型#includepthread.hpthread_tpthread_self(void);参数说明无参数直接调用就行。返回值含义pthread_t当前线程的 ID永远成功不会失败 打印线程 ID 用%lu格式符pthread_t 本质上是 unsigned long。代码示例#includestdio.h#includepthread.hvoid*thread_func(void*arg){// 获取当前线程子线程的 IDpthread_ttidpthread_self();printf(子线程的 ID 是 %lu\n,tid);returnNULL;}intmain(){pthread_tthread_id;intret;// 创建子线程retpthread_create(thread_id,NULL,thread_func,NULL);if(ret!0){printf(创建线程失败\n);return-1;}// 获取主线程自己的 IDpthread_tmain_tidpthread_self();printf(主线程的 ID 是 %lu\n,main_tid);// 等子线程跑完pthread_join(thread_id,NULL);printf(主线程结束\n);return0;}5. 终止线程pthread_exit线程怎么结束有几种方式线程函数return返回最常用调用pthread_exit()主动退出被其他线程取消pthread_cancelpthread_exit就像进程里的exit但它只终止当前线程不影响同进程的其他线程。函数原型#includepthread.hvoidpthread_exit(void*retval);参数说明参数数据类型详细说明使用规则与补充retvalvoid *线程退出的返回值可以被 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 */#includestdio.h#includestdlib.h#includepthread.h#includeunistd.hvoid*thread_func(void*arg){printf(子线程开始执行\n);sleep(3);// 模拟干活睡 3 秒printf(子线程执行完毕\n);// 线程结束返回一个值pthread_exit((void*)我是子线程的返回值);}intmain(){pthread_ttid;intret;retpthread_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函数。函数原型#includepthread.hintpthread_cancel(pthread_tthread);参数说明参数数据类型详细说明使用规则与补充threadpthread_t要取消的目标线程 ID返回值含义0成功只是成功发送了取消请求非 0失败注意事项pthread_cancel只是发送取消请求不保证线程一定会被取消线程默认是可以被取消的但它会等到某个取消点才真正退出比如 sleep、read、write 这些系统调用就是取消点如果线程一直在纯计算没有系统调用可能收不到取消请求代码示例#includestdio.h#includestdlib.h#includepthread.h#includeunistd.hvoid*thread_function(void*arg){inti;printf(子线程开始运行\n);// 循环打印每次 sleep 1 秒// sleep 是取消点所以取消请求能在这里生效for(i1;i5;i){printf(子线程打印%d\n,i);sleep(1);}printf(子线程正常结束\n);returnNULL;}intmain(){pthread_tthread;intres;// 创建子线程respthread_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。函数原型#includepthread.hintpthread_detach(pthread_tthread);参数说明参数数据类型详细说明使用规则与补充threadpthread_t要设置为分离状态的线程 ID返回值含义0成功非 0失败核心要点分离状态的线程终止后系统自动回收资源无需 pthread_join一旦设为分离状态就不能再转回非分离也不能再 join 了也可以在创建线程时通过属性直接设为分离状态代码示例#includestdio.h#includestdlib.h#includepthread.h#includeunistd.hvoid*thread_func(void*arg){inti;for(i0;i5;i){printf(子线程打印%d\n,i);sleep(1);}printf(子线程结束系统自动回收资源\n);returnNULL;}intmain(){pthread_ttid;intret;// 创建线程retpthread_create(tid,NULL,thread_func,NULL);if(ret!0){printf(创建线程失败\n);exit(EXIT_FAILURE);}// 设置为分离状态retpthread_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 万吗代码示例有问题的版本#includestdio.h#includestdlib.h#includepthread.h#defineTHREAD_NUM2// 两个线程intcount0;// 全局计数器// 每个线程都对 count 加 10 万次void*thread_func(void*arg){inti;for(i0;i100000;i){count;// 计数器加 1}pthread_exit(NULL);}intmain(){pthread_tthread_id[THREAD_NUM];intresult,i;// 创建两个线程for(i0;iTHREAD_NUM;i){resultpthread_create(thread_id[i],NULL,thread_func,NULL);if(result!0){printf(Error: pthread_create failed.\n);exit(EXIT_FAILURE);}}// 等待两个线程都结束for(i0;iTHREAD_NUM;i){resultpthread_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是最常用的线程同步机制。简单说就是访问共享资源前先加锁访问完再解锁。同一时刻只有一个线程能拿到锁其他线程就得等着。互斥锁使用步骤步骤函数作用1pthread_mutex_t mutex定义互斥锁变量2pthread_mutex_init()初始化互斥锁3pthread_mutex_lock()加锁拿不到就阻塞等4访问共享资源临界区5pthread_mutex_unlock()解锁6pthread_mutex_destroy()销毁互斥锁pthread_mutex_init 的函数原型#includepthread.hintpthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*attr);参数说明参数数据类型详细说明使用规则与补充mutexpthread_mutex_t *互斥锁指针attrconst 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 所有线程相关函数 */#includestdio.h#includestdlib.h#includepthread.h#defineTHREAD_NUM2intcount0;pthread_mutex_tmutex;// 定义互斥锁变量void*thread_func(void*arg){inti;for(i0;i100000;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(i0;iTHREAD_NUM;i){resultpthread_create(thread_id[i],NULL,thread_func,NULL);if(result!0){printf(Error: pthread_create failed.\n);exit(EXIT_FAILURE);}}// 等待线程结束for(i0;iTHREAD_NUM;i){resultpthread_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 份有疑问或是建议的博友们可以评论或私信我我看到会及时解答或改正的。