Hello大家好:
这是一篇面向嵌入式 Linux 应用开发者的线程系统总结。
里面涉及到的部分有:线程相关概念、API、同步、工程设计、排错调优、笔试题、面试题、编程题。
希望大伙儿阅读这篇文章时,不是单纯“背几个 pthread 函数”,而是把 线程基础、同步机制、协作模型、工程化设计、调试排错、常考题都能熟练运用起来。
先说结论:线程到底学什么?
很多朋友在一开始学线程时,容易陷入两个误区:
第一,只记 API。 第二,只会写 demo,不会做工程。
而嵌入式 Linux 线程真正要掌握的,是下面这条主线:
创建线程 → 理解共享 → 发现竞争 → 用锁保护 → 用条件变量协作 → 做生产者消费者 → 做工程化线程模型 → 能调试和排错
也就是说,线程不是孤立的语法点,而是一整套并发设计能力。
话不多说,咱们直接进入主题。
在嵌入式 Linux 应用开发中,线程通常用于这些场景:
串口接收线程
网络收发线程
传感器采集线程
协议解析线程
数据处理线程
日志线程
存储线程
心跳/看门狗线程
UI 线程或业务线程
后台维护线程
为什么要用线程?
因为嵌入式系统中,不同任务的特征往往不一样:
有的任务强调实时采集
有的任务计算量大
有的任务会阻塞在 I/O
有的任务要长期后台运行
有的任务只能异步处理
如果全部写在一个主循环里,代码容易变得阻塞、耦合、难维护。 这时线程就提供了一种非常重要的组织方式:把不同职责拆开并发执行。
进程是系统进行资源分配的基本单位。 每个进程拥有自己独立的地址空间、资源、文件描述符表、内存映射等。
线程是 CPU 调度的基本单位。 线程运行在进程内部,同一个进程可以拥有多个线程。
同一进程中的多个线程共享:
代码段
全局变量
静态变量
堆
文件描述符
地址空间
信号处理设置
每个线程私有:
线程栈
寄存器上下文
程序计数器
线程 ID
线程局部存储
因为线程不需要重新建立完整的地址空间,创建和切换开销通常小于进程。 这也是线程适合做高频协作任务的重要原因。
很多人把这两个词混着用,其实不一样。
多个任务在时间上交替推进。 单核 CPU 也可以并发,因为线程会被调度器轮流执行。
多个任务在同一时刻真正同时执行。 通常依赖多核 CPU。
单核平台:主要是并发
多核平台:可以并发,也可以并行
这意味着线程程序的执行顺序往往不确定。 所以多线程问题的本质,往往不是“语法错”,而是“时序不可预测”。
嵌入式 Linux 应用层最常用的是 POSIX 线程,也就是 pthread。
头文件:
#include <pthread.h>
编译时通常需要链接线程库:
gcc test.c -o test -lpthread
有些环境也常见:
gcc test.c -o test -pthread
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);参数解释:
thread:返回线程 ID
attr:线程属性,通常先传 NULL
start_routine:线程入口函数
arg:传给线程函数的参数
线程函数的标准形式
void *thread_func(void *arg){ return NULL;}最简示例
#include <stdio.h>#include <pthread.h>void *worker(void *arg){printf("worker thread running\n");return NULL;}int main(void){ pthread_t tid; pthread_create(&tid, NULL, worker, NULL); pthread_join(tid, NULL);return 0;}函数原型:
int pthread_join(pthread_t thread, void retval);
作用:
等待指定线程结束
回收线程资源
获取线程返回值
如果不回收 joinable 线程,线程资源可能泄漏。
方式一:线程函数 return
最自然、最常见。
方式二:pthread_exit
显式退出当前线程。
方式三:被取消
由其他线程调用 pthread_cancel 请求取消。
如果线程不需要被 join,可以分离。
pthread_detach(tid);
特点:
线程结束后自动释放系统资源
分离后不能再 pthread_join
适用场景
后台工作线程、无需返回值的独立线程。
不适用场景
主线程需要知道它何时结束,或者要获取它的返回值。
线程入口只有一个参数:
*void arg
所以所有参数传递都要围绕这个 void * 展开。
int num = 100;pthread_create(&tid, NULL, worker, &num);线程中:
void *worker(void *arg){ int value = *(int *)arg;return NULL;}注意:要保证这个地址在线程使用期间有效。
typedef struct{ int id; char name[32]; int interval_ms; }thread_arg_t;传入线程:
thread_arg_t arg;pthread_create(&tid, NULL, worker, &arg);线程中:
thread_arg_t *p = (thread_arg_t *)arg;为什么结构体最常用?
因为线程函数只能接收一个参数,而真实业务常常不止一个参数。
错误示例:
for (i = 0; i < 5; i++) { pthread_create(&tid[i], NULL, worker, &i);}这会导致多个线程拿到同一个 i 的地址,结果经常错乱。
正确做法:
给每个线程单独准备参数
用数组保存每个参数
或为每个线程动态分配参数
线程返回值本质是 void *。
void *worker(void *arg){ int *result = malloc(sizeof(int)); *result = 123;return result;}主线程接收:
void *ret = NULL;pthread_join(tid, &ret);int value = *(int *)ret;free(ret);
重要警告:不能返回局部变量地址
错误示例:
void *worker(void *arg){int result = 123;return &result; // 错误}
因为局部变量在函数返回后生命周期结束,返回其地址是典型野指针错误。
线程之间为什么通信方便? 因为它们共享同一个进程地址空间。
这意味着:
主线程改全局变量,子线程能看到
子线程往堆里写数据,其他线程也可能访问
这很方便,但也很危险。
共享数据的典型类型
全局变量
静态变量
堆上的对象
链表、队列、哈希表
全局配置结构体
socket/串口句柄状态
业务状态机变量
真正的风险:多个线程同时操作同一份数据
如果多个线程并发访问共享变量,而且至少有一个线程在写,那么如果没有同步保护,就可能发生:
数据竞争
丢失更新
中间态被读到
内存破坏
时序依赖 bug
很多初学者觉得下面这句没问题:
counter++;
但实际上它通常不是一个原子操作,而是可以分成:
读 counter 加 1
写回去
如果两个线程同时做,就可能这样:
线程 A 读到 100
线程 B 也读到 100
A 写回 101
B 写回 101
结果本来应该加两次,最后只加了一次。
这就是典型的数据竞争。
互斥锁的作用是:
让同一时刻只有一个线程进入临界区,从而保护共享数据。
pthread_mutex_init
pthread_mutex_lock
pthread_mutex_unlock
pthread_mutex_destroy
pthread_mutex_lock(&mutex);/* 临界区:访问共享数据 */pthread_mutex_unlock(&mutex);
凡是访问共享资源的那段代码,都可能是临界区。 例如:
修改全局计数器
入队、出队
修改链表
更新状态机
操作共享缓存
锁的本质目标是正确性。 性能优化一定是在正确性成立之后再做。
结果:其他线程永远阻塞。
如果你把耗时逻辑放在锁内,比如:
文件写入
网络发送
长时间计算
大量打印
就会导致其他线程长时间等锁,系统吞吐下降。
锁范围不完整,就可能保护不到真正的共享状态。
线程 A:先锁 A 再锁 B 线程 B:先锁 B 再锁 A
这种情况非常容易死锁。
例如锁内等待 I/O,风险很大。
两个或多个线程互相等待对方持有的资源,导致谁也无法继续。
线程 A:
已持有锁 1
等锁 2
线程 B:
已持有锁 2
等锁 1
最终互相卡住。
原则一:统一加锁顺序
所有线程都按同样顺序加锁。
原则二:缩短持锁时间
越短越不容易出问题。
原则三:不要在锁内做复杂和阻塞操作
尤其是 I/O。
原则四:必要时使用 trylock 或超时机制
帮助诊断和规避问题。
互斥锁解决的是:
同一时刻只能一个线程访问共享资源。
但很多场景下还需要:
条件不满足时先睡眠,等别人通知我再继续。
这就是条件变量的作用。
pthread_cond_init
pthread_cond_wait
pthread_cond_signal
pthread_cond_broadcast
pthread_cond_destroy
pthread_cond_wait(&cond, &mutex);
这句最关键的语义是:
当前线程释放 mutex
当前线程进入等待
被唤醒后重新竞争 mutex
抢到 mutex 后从 wait 后面继续执行
这是一条必须彻底理解的知识点。
因为等待某个条件本质上是在等“共享状态变化”,而共享状态必须在锁的保护下检查和修改,否则容易:
丢通知
状态不一致
被并发篡改
时序错乱
标准模板:
pthread_mutex_lock(&mutex);while (ready == 0) { pthread_cond_wait(&cond, &mutex);}pthread_mutex_unlock(&mutex); //记得解锁为什么是 while?
因为:
可能发生虚假唤醒
多线程被唤醒后需要再次检查条件
线程醒来时条件可能已被别的线程改掉
pthread_cond_signal
唤醒一个等待线程。
pthread_cond_broadcast
唤醒所有等待线程。
用 signal
当你明确知道:这次变化只够一个线程继续执行。 例如队列里只新增了一个数据。
用 broadcast
当你要让所有等待线程都醒来重新检查条件。 例如程序退出、系统模式切换、初始化完成。
这是线程模块最重要的工程模型之一。
生产者:往共享缓冲区放数据
消费者:从共享缓冲区取数据
配套通常需要:
一个共享队列
一个 mutex
not_empty 条件变量
not_full 条件变量
因为它把“线程间协作”最关键的几个问题都串起来了:
共享数据保护
队列状态管理
条件等待与通知
线程解耦
流水线设计基础
队列空了,消费者要等待
队列满了,生产者要等待
入队出队必须在锁保护下完成
修改状态后要发出正确通知
因为有两个不同的等待条件:
非空:消费者依赖
非满:生产者依赖
因此通常设计成:
not_empty
not_full
这样语义清晰、逻辑稳定。
在嵌入式 Linux 线程开发中,信号量也常见。
头文件:
#include <semaphore.h>
信号量本质上表示“可用资源计数”。
例如:
有多少个可用缓冲区
当前允许几个线程同时进入
有几个任务待处理
sem_init
sem_wait
sem_post
sem_destroy
mutex
强调“互斥访问”,一次只能一个线程持有。
semaphore
强调“资源数量”,可以允许多个线程通过,取决于计数值。
资源池
限流
任务计数
简单同步通知
某些生产者消费者实现
读写锁的思想是:
多个读线程可以并发进入
写线程必须独占
常用 API:
pthread_rwlock_init
pthread_rwlock_rdlock
pthread_rwlock_wrlock
pthread_rwlock_unlock
pthread_rwlock_destroy
适用场景:
配置表
查询表
数据字典
读远多于写的共享状态
如果写很多,读写锁不一定有优势。
线程属性对象:
pthread_attr_t
常见可配置项
joinable
detached
非常重要,嵌入式尤其要关注。
例如:
SCHED_OTHER
SCHED_FIFO
SCHED_RR
线程优先级直接影响实时性和调度结果。
每个线程都有自己的栈。 如果线程里有这些情况,就要特别警惕栈溢出:
大数组放在局部变量里
局部结构体很大
函数调用层次深
递归
同时调用很多库函数
常见建议 建议一
大对象尽量放堆上,不要全放栈里。
建议二
线程栈大小要结合业务评估。
建议三
线程多的时候,默认栈过大也可能浪费内存。
线程取消 API:
pthread_cancel(tid);
它不是“立刻强杀”,而是发出取消请求。 线程是否马上退出,取决于:
取消状态
取消类型
是否到达取消点
为什么说不要滥用?
因为异步取消容易导致:
锁没释放
内存没释放
文件没关闭
状态不一致
工程中更稳妥的方式通常是:
设置退出标志
唤醒阻塞线程
让线程自己清理后退出
这叫优雅退出。
线程局部存储适用于:
每线程上下文
每线程日志标签
每线程错误码
每线程缓存
常用 API:
pthread_key_create
pthread_setspecific
pthread_getspecific
pthread_key_delete
优点是: 看起来像全局访问,但实际上每个线程拿到的是自己的数据。
在普通 PC 应用中,大家更关注功能。 在嵌入式 Linux 中,还必须关注调度实时性。
SCHED_OTHER 普通时间片调度,最常见。
SCHED_FIFO 实时先进先出。
SCHED_RR 实时轮转调度。
例如:
采集线程不能被低价值线程拖住
日志线程不应该抢占关键控制线程
高优先级线程如果设计不当,可能饿死低优先级线程
经典问题:
低优先级线程持有锁
高优先级线程等待这把锁
中优先级线程不断抢占 CPU
低优先级线程反而得不到机会释放锁
高优先级线程被长期拖住
解决思路:
缩短锁持有时间
使用优先级继承机制
避免高优先级线程依赖长临界区
线程可以绑定到指定 CPU 核心运行。 常见用途:
减少迁核
提升缓存命中
改善实时性
把关键线程和非关键线程隔离开
多核嵌入式系统中,这一项很有价值。
线程常常阻塞在这些地方:
read
write
recv
accept
select
poll
epoll_wait
pthread_cond_wait
这时如果只是简单设置:
g_stop = 1;
线程往往不会立刻退出,因为它还睡在阻塞点里。
工程中的常见解决办法
使用超时等待
关闭 fd 打断阻塞
用管道/eventfd 唤醒
用条件变量广播
非阻塞 I/O + 轮询
统一事件通知机制
单纯会 pthread_create,还不算会线程。 工程里真正重要的是“怎么组织线程”。
例如:
采集线程 → 解析线程 → 处理线程 → 日志线程
优点:
职责清晰
解耦明显
易扩展
易定位问题
主线程负责:
epoll
接收外部事件
分发任务
工作线程负责:
执行业务逻辑
处理任务队列
例如:
前台线程:采集、控制、协议收包
后台线程:日志、存盘、上传、统计
这样关键线程不容易被慢操作拖死。
用于:
线程心跳检测
异常监控
队列积压监控
卡死检测
这部分比单个 API 更重要。
原则一:尽量减少共享
共享越多,同步越复杂,bug 越多。 能通过消息传递解决的,就不要到处共享全局状态。
原则二:共享资源要封装
例如队列应该封装成:
queue_init
queue_push
queue_pop
queue_destroy
而不是让所有线程都直接操作:
head
tail
count
lock
cond
原则三:锁内只做最小必要操作
锁内做:
判空
判满
入队
出队
修改共享状态
锁外做:
复杂计算
文件写入
网络发送
printf
耗时业务逻辑
原则四:明确对象所有权
要约定清楚:
谁申请
谁使用
谁释放
否则很容易:
野指针
重复释放
内存泄漏
多线程抢释放
原则五:提前设计退出机制
线程系统最怕的不是“跑不起来”,而是“退不出来”。
很多 demo 都写成:
while (1) { ...}但真实工程里一定要考虑退出。
一个典型的优雅退出流程
例如:
g_stop = 1;
例如:
pthread_cond_broadcast
关闭 fd
发事件
写 pipe/eventfd
满足退出条件就收尾退出。
确保没有线程资源泄漏。
这才叫工程级退出。
先问自己四个问题:
谁在等谁?
谁拿着锁?
谁阻塞在 I/O?
谁还没退出?
重点排查:
pthread_join
pthread_cond_wait
read/recv/select/poll
多把锁交叉等待
典型原因:
忙等待
死循环
自旋
高频打印
非阻塞轮询写法不合理
重点看:
是否访问了共享数据
是否加锁
锁范围是否正确
是否有多个线程同时写同一对象
是否传了无效地址
常见原因:
栈溢出
野指针
空指针
重复释放
参数生命周期错误
线程创建成功但没 join 也没 detach
返回局部变量地址
多线程共享同一参数地址
counter++ 未加锁
用 if 包 pthread_cond_wait
修改条件时不加锁
只改标志位,不唤醒等待线程
多个线程等同一 cond 却误以为 signal 会全醒
在锁内做 I/O
锁顺序不一致导致死锁
线程栈设置不合理
退出时没有统一回收机制
多线程同时操作同一个 fd 却没有设计约束
为了快,直接大量用全局变量共享状态
只会写 demo,不会封装队列和模块边界
你可以把线程模块总结成这 8 个大板块:
create
join
detach
exit
参数
返回值
线程共享地址空间
数据竞争
原子性
临界区
mutex
cond
semaphore
rwlock
TLS
条件等待
signal / broadcast
生产者消费者
队列同步
流水线模型
工作线程模型
日志线程
监控线程
退出机制
优先级
调度策略
亲和性
优先级反转
死锁
卡死
高 CPU
栈溢出
野指针
缩短临界区
减少共享
控制线程数
合理分工
减少不必要上下文切换
下面这部分适合复习、刷题、面试前突击。
笔试题 1 进程和线程的主要区别是什么?
参考答案
进程是资源分配的基本单位,线程是 CPU 调度的基本单位。 同一进程中的线程共享地址空间、全局变量、堆和文件描述符,但每个线程拥有独立的栈、寄存器上下文和线程 ID。 线程创建和切换开销通常小于进程,但线程间共享资源更多,因此同步问题更突出。
笔试题 2 pthread_create 的作用和四个参数分别是什么?
参考答案
pthread_create 用于创建一个新线程。 四个参数分别是:
返回线程 ID 的指针
线程属性,通常传 NULL
线程入口函数
传递给线程函数的参数**
笔试题 3 为什么线程函数的参数类型和返回值类型都是 void *?
参考答案
因为线程函数需要支持通用参数和通用返回值。 void * 可以承载任意类型指针,调用者和线程函数通过强制类型转换实现灵活传参和返回结果。
笔试题 4 为什么不能返回局部变量地址?
参考答案
局部变量位于函数栈帧中,函数返回后生命周期结束,其地址失效。 如果返回局部变量地址,调用方继续访问就会产生未定义行为。
笔试题 5 什么是数据竞争?
参考答案
多个线程并发访问同一共享数据,并且至少有一个线程执行写操作,同时访问之间没有同步保护,这种情况称为数据竞争。 数据竞争会导致结果不可预测。
笔试题 6 counter++ 为什么不是线程安全的?
参考答案
因为 counter++ 一般包含读、改、写三个步骤,不是原子操作。 多个线程同时执行时可能发生丢失更新。
笔试题 7 什么是临界区?
参考答案
访问共享资源的代码区域称为临界区。 为了避免并发访问导致的数据错误,临界区通常需要用互斥锁保护。
笔试题 8 pthread_mutex_lock 和 pthread_mutex_unlock 的作用是什么?
参考答案
pthread_mutex_lock 用于获取互斥锁,进入临界区。 pthread_mutex_unlock 用于释放互斥锁,让其他线程有机会进入临界区。 它们共同用于保护共享数据。
笔试题 9 条件变量的作用是什么?
参考答案
条件变量用于线程间协作。 当条件不满足时,线程可以在条件变量上等待;当其他线程修改共享状态使条件满足后,通过 signal 或 broadcast 唤醒等待线程。
笔试题 10 为什么 pthread_cond_wait 必须和 mutex 一起使用?
参考答案
因为线程等待的“条件”本质上依赖共享状态,而共享状态必须在锁保护下检查和修改。 pthread_cond_wait 会原子地释放 mutex 并进入等待,被唤醒后再重新获取 mutex,从而保证条件检查和等待之间不会出现竞态。
笔试题 11 为什么等待条件变量时要用 while 而不是 if?
参考答案
因为线程可能被虚假唤醒,也可能被唤醒后发现条件已经被其他线程改变。 因此线程醒来后必须再次检查条件,所以标准写法是 while。
笔试题 12 pthread_cond_signal 和 pthread_cond_broadcast 有什么区别?
参考答案
pthread_cond_signal 唤醒一个等待线程。 pthread_cond_broadcast 唤醒所有等待线程。 通常当一次状态变化只够一个线程继续时用 signal, 当所有线程都需要重新检查条件用broadcast。
笔试题 13 什么是死锁?如何避免?
参考答案
死锁是指多个线程互相等待对方占有的资源,导致无法继续执行。 避免方法包括:
统一加锁顺序
缩短持锁时间
避免锁内阻塞操作
减少多锁嵌套
笔试题 14 什么是优先级反转?
参考答案
高优先级线程等待低优先级线程持有的锁,而中优先级线程不断运行,导致低优先级线程无法及时释放锁,从而让高优先级线程被间接阻塞,这种现象叫优先级反转。
笔试题 15 什么是优雅退出?
参考答案
优雅退出是指线程在接收到退出请求后,不是立即被暴力结束,而是按照约定流程:
设置退出标志
唤醒阻塞线程
完成必要清理
正常退出
被主线程回收
从而保证资源一致性和系统稳定性。
面试题 1 在嵌入式 Linux 开发中,什么时候适合用线程,什么时候更适合用进程?
参考答案
当多个任务需要高效共享数据、切换开销要尽量小、协作非常频繁时,更适合用线程。 当系统需要更强的故障隔离、模块独立部署、崩溃影响要可控时,更适合用进程。 嵌入式系统中,很多应用层采集、处理、日志、通信模块常用线程实现;而对安全隔离要求更高的服务拆分常用进程方案。
面试题 2 你在项目里是怎么设计线程模型的?
参考答案
我通常不会让多个线程直接共享大量全局变量,而是按职责划分线程,例如采集线程、处理线程、日志线程,通过线程安全队列传递数据。 共享队列内部封装 mutex 和条件变量,线程之间以消息或数据块传递为主。 同时会设计统一的退出机制,包括停止标志、阻塞唤醒和 join 回收,确保线程系统可控可维护。
面试题 3 讲一下你对 mutex 和 cond 的理解。
参考答案
mutex 解决的是互斥问题,保证同一时刻只有一个线程访问共享状态。 cond 解决的是协作问题,当条件不满足时线程睡眠等待,条件满足后由其他线程通知。 两者经常配合使用:mutex 保护共享条件,cond 实现等待和唤醒。 使用时要遵循“加锁、while 检查条件、wait、条件满足继续执行”的模板。
面试题 4 为什么修改条件后一般要先改状态,再发 signal?
参考答案
因为线程等待的是“共享状态变化后的条件成立”,所以应该先在锁保护下修改共享状态,再通知等待线程。 这样被唤醒的线程醒来后重新检查条件时,才能看到一致的已更新状态。
面试题 5 多个线程等待同一个条件变量时,能否指定唤醒某一个固定线程?
参考答案
使用同一个条件变量调用 pthread_cond_signal 时,只能唤醒其中一个等待线程,但具体是哪一个线程通常不可控。 如果业务上需要定向唤醒某个指定线程,更合理的设计是:
每个线程拥有独立条件变量
或每个线程拥有自己的任务队列
或通过事件分发机制实现定向通知
面试题 6 线程为什么会卡死?你怎么排查?
参考答案
线程卡死通常有几类原因:
死锁
阻塞在 cond wait 或 I/O
主线程 join 等待未退出线程
退出标志设置了但没有唤醒阻塞线程
排查时我会先定位线程状态,看它阻塞在哪个调用点;再看锁持有情况、条件变量等待条件、I/O 状态和退出流程。 如果有日志,我会重点看锁前锁后、入队出队、等待唤醒、退出路径。
面试题 7 生产者消费者模型在工程里有什么意义?
参考答案
生产者消费者模型本质上是一种线程解耦机制。 它将数据产生和数据处理分开,通过线程安全队列连接,既能提升模块边界清晰度,也能降低直接共享状态带来的复杂度。 在嵌入式项目中,串口收包与协议解析、传感器采集与算法处理、业务处理与日志落盘都适合这种模型。
面试题 8 高优先级线程为什么不应该在锁内做耗时操作?
参考答案
高优先级线程如果长时间持锁,会导致其他线程无法进入临界区,放大系统阻塞范围。 如果它在锁内做文件写入、网络发送、复杂计算等耗时操作,还会进一步影响系统实时性,甚至引发优先级反转和响应抖动。 所以高优先级线程应尽量缩短临界区,只在锁内完成共享状态最小修改。
面试题 9 线程退出时你最关注什么?
参考答案
我最关注三件事:
第一,线程是否可能阻塞在 wait 或 I/O。 第二,退出时是否能被可靠唤醒。 第三,资源是否能被一致性回收。
我通常会设计停止标志、唤醒机制、线程回收流程和资源销毁顺序,避免出现线程没退干净、锁没释放、fd 没关闭、对象重复释放等问题。
面试题 10 嵌入式 Linux 线程开发中,你认为最容易被忽略的问题是什么?
参考答案
我认为最容易被忽略的是:
线程栈大小
退出机制
锁内耗时操作
参数生命周期
多线程共享 fd 的使用规则
很多程序能跑起来,但在长时间运行、异常退出、高负载或者边界条件下出问题,根源往往就在这些细节。
下面给你一组从基础到工程的线程编程题。 这些题很适合练习、笔试、面试上机和项目复盘。
编程题 1:创建一个线程并等待它结束
题目要求
编写一个程序,创建一个子线程,子线程打印 "hello thread",主线程等待其结束后打印 "main exit"。
考察点
pthread_create
线程函数格式
pthread_join
编程题 2:创建 5 个线程,给每个线程传不同的编号
题目要求
创建 5 个线程,每个线程打印自己的线程编号和 pthread ID。
考察点
多线程创建
参数传递
循环变量地址问题
提示
不要直接把循环变量 i 的地址传进去。
编程题 3:线程返回计算结果
题目要求
创建一个线程,计算 1 到 100 的和,并将结果返回给主线程打印。
考察点
线程返回值
pthread_join
堆内存返回结果
编程题 4:两个线程同时累加全局计数器
题目要求
两个线程分别将全局变量 counter 累加 100000 次。 先写一个未加锁版本,再写一个加锁版本,比较结果。
考察点
数据竞争
counter++ 非线程安全
mutex 的使用
编程题 5:一个线程等待,主线程唤醒
题目要求
子线程在 ready == 0 时等待;主线程 3 秒后设置 ready = 1 并唤醒子线程。
考察点
条件变量
pthread_cond_wait
while 检查条件
signal 通知
编程题 6:两个线程同时等待,比较 signal 与 broadcast
题目要求
创建两个子线程,它们都在等待同一个条件变量。 主线程分别测试:
用 pthread_cond_signal
用 pthread_cond_broadcast
观察输出差异。
考察点
多线程等待同一条件变量
signal 与 broadcast 的区别
编程题 7:实现一个固定长度的生产者消费者模型
题目要求
使用一个长度为 5 的环形缓冲区,实现:
一个生产者线程
一个消费者线程
生产者生产 10 个整数,消费者消费 10 个整数。
考察点
环形队列
mutex
条件变量
not_empty
not_full
编程题 8:给生产者消费者增加退出机制
题目要求
在上一题基础上增加 done 或 stop 标志。 当生产者生产完毕后,消费者不能永久卡在等待状态,而是能够正常退出。
考察点
优雅退出
broadcast
阻塞线程退出条件设计
编程题 9:三线程流水线模型
题目要求
设计三个线程:
采集线程:每 200ms 产生一个整数
处理线程:将数据乘 10
日志线程:输出处理结果
线程之间通过两个线程安全队列连接。
考察点
工程化线程设计
数据流解耦
多级生产者消费者
退出机制
编程题 10:日志线程异步化
题目要求
主线程不断产生日志消息,日志线程负责异步写文件。 要求主线程不能因为文件 I/O 被严重阻塞。
考察点
异步日志思想
队列缓冲
锁粒度
I/O 与业务解耦
编程题 11:多线程访问共享配置,使用读写锁
题目要求
多个读线程频繁读取配置,一个写线程偶尔更新配置。 要求设计读写锁保护。
考察点
pthread_rwlock
读多写少场景建模
编程题 12:设计一个支持优雅退出的阻塞收包线程
题目要求
设计一个网络接收线程,它会阻塞在 recv 或 select 上。 要求程序退出时,该线程能够被唤醒并正常退出。
考察点
阻塞 I/O
停止标志
唤醒机制
工程级退出设计
如果你想把线程真正学到“能做项目”的程度,我建议按下面顺序推进:
第一阶段:基础必会
创建线程
join / detach
参数传递
返回值
多线程并发现象
第二阶段:同步必会
共享变量
数据竞争
mutex
死锁
锁粒度
第三阶段:协作必会
条件变量
signal / broadcast
生产者消费者
退出机制
第四阶段:工程必会
线程安全队列
三线程流水线
异步日志
阻塞 I/O 退出
监控线程
第五阶段:进阶必会
信号量
读写锁
TLS
优先级
调度策略
CPU 亲和性
性能优化与排错
线程共享地址空间,通信方便,但同步复杂。
线程问题的根源,往往不是函数不会写,而是时序不可预测。
只要有共享写,就必须考虑同步。
mutex 解决互斥,cond 解决协作。
pthread_cond_wait 的本质是:释放锁、睡眠、被唤醒后重新加锁。
等待条件变量必须用 while,而不是 if。
signal 适合唤醒一个,broadcast 适合唤醒全部。
线程工程化的关键,不是更多全局变量,而是更少共享、更清晰边界。
退出机制必须事先设计,否则线程系统迟早卡死。
真正的线程高手,不仅会写代码,更会设计、排错和收尾。