一、并发的本质
1. 并发≠并行
首先需要明确两个核心概念:
- 并发(Concurrency):多个任务在CPU时间片上交替执行,宏观上“同时”运行,微观上是串行切换。
- 并行(Parallelism):多个任务在多个物理CPU核心上真正地同时执行。
在单核ARM处理器上运行的多线程程序,实现的是并发;而在多核处理器上,才可能实现并行。但无论在哪种场景下,只要多个执行流需要访问共享资源,就必然要面对竞态条件(Race Condition) 这一核心挑战。
2. 竞态的根源
竞态条件的根源在于多个执行流对共享资源的访问顺序具有不确定性。解决这一问题,正是多线程编程的核心。
这种不确定性主要由以下几种情况导致:
(1)高级语言指令与机器指令的差异
一段看似简单的C语言代码,在编译后可能对应多条机器指令。例如下面这个常见的计数器递增操作:

counter++ 这个操作编译后通常对应三条底层指令:
LOAD counter → 寄存器
ADD 寄存器 + 1
STORE 寄存器 → counter
当两个线程各自执行10万次counter++操作时,理论结果应为20万。但实际测试结果往往在13万到18万之间波动。这正是因为两个线程的这三条指令可能发生“交叉执行”,导致部分递增操作被覆盖。
(2)编译器和CPU的优化行为
为了提高性能,编译器可能会对指令进行重排序,现代CPU也普遍支持乱序执行。这意味着我们编写的代码顺序,并不一定是其在处理器上实际执行的顺序。
(3)多核CPU的缓存一致性挑战
在多核处理器中,每个核心通常拥有自己独立的L1/L2缓存。对一个内存地址的修改,并不会立即同步到其他核心的缓存中,这就是缓存一致性问题。这种底层硬件的复杂性进一步加剧了多线程编程的难度,要求开发者深入理解Linux系统的并发模型与内存屏障机制。
二、POSIX线程库的三大同步原语
为了应对并发挑战,POSIX线程库提供了几种核心的同步机制,主要包括互斥锁、条件变量和读写锁。
2.1 互斥锁
互斥锁的语义非常直观:在任何时刻,只允许一个线程持有该锁。

一个至关重要的实践原则是:锁的粒度要尽可能小。
如果将整个复杂的业务逻辑都包裹在锁的保护范围内,多线程程序就会退化为“排队执行”,其性能可能反而不如设计良好的单线程程序。
错误示范:锁粒度过大

正确做法:仅锁定对共享数据的访问
应该将计算逻辑等耗时操作移出临界区,锁只用来保护对共享变量的读写操作。

2.2 条件变量
考虑经典的生产者-消费者场景:生产者线程产生数据,消费者线程处理数据。消费者如何高效地获知“数据已就绪”呢?条件变量是解决此类线程间等待与通知场景的最佳工具。它允许线程在条件不满足时主动休眠,当条件满足时被精准唤醒,从而在保证实时响应的同时,最大程度地降低CPU的空转消耗。
低效方案:忙等待(轮询)

高效方案:使用条件变量
使用条件变量的标准模式如下:
等待方(消费者)步骤:
- 获取关联的互斥锁(
pthread_mutex_lock)。
- 使用
while循环检查条件是否满足(例如:while(queue.empty()))。
- 若条件不满足,调用
pthread_cond_wait()进入等待状态(该函数会自动释放互斥锁并休眠)。
- 被唤醒后,
pthread_cond_wait会重新获取互斥锁,并再次检查条件,条件满足后执行业务逻辑。
- 释放互斥锁(
pthread_mutex_unlock)。
通知方(生产者)步骤:
- 获取关联的互斥锁(
pthread_mutex_lock)。
- 修改条件(例如:向队列添加数据,设置标志位)。
- 发送通知(
pthread_cond_signal 唤醒一个等待线程,或 pthread_cond_broadcast 唤醒所有等待线程)。
- 释放互斥锁(
pthread_mutex_unlock)。
代码示例:

为什么必须使用while循环而非if判断?
这是因为存在“虚假唤醒”(spurious wakeup) 的可能性——线程可能会在没有收到任何signal的情况下从pthread_cond_wait中返回。这是POSIX标准允许的行为,使用while循环可以强制进行条件复查,确保程序的正确性。
2.3 读写锁
如果你的应用场景符合“读多写少”的模式(例如90%是读操作,10%是写操作),使用互斥锁会显得非常低效,因为读操作之间本不需要互斥。

读写锁允许多个线程同时持有“读锁”进行读取,但“写锁”是独占的。当有线程持有写锁时,其他所有读写线程都必须等待。
下图清晰地对比了三种同步原语的典型适用场景:

三、死锁及其预防
死锁是多线程编程中最经典也是最棘手的问题之一。形成死锁必须同时满足以下四个条件(Coffman条件):
- 互斥:资源不能被共享,一次只能被一个线程占用。
- 持有并等待:一个线程在持有一个资源的同时,等待获取另一个资源。
- 不可抢占:资源不能被强制从持有它的线程中剥夺。
- 循环等待:存在一个线程等待链,每个线程都在等待下一个线程所占有的资源。
破坏以上任意一个条件即可预防死锁。在工程实践中,最有效且常用的方法是破坏“循环等待”条件,即规定全局的加锁顺序。
一个经典的AB-BA死锁示例如下:

该程序会陷入死锁,CPU占用率降为0。问题在于两个线程试图以相反的顺序(Thread1: A->B, Thread2: B->A)获取两把锁,在特定的调度时序下,它们会互相等待对方已持有的锁。
执行时序图示:

修复方案:统一加锁顺序
强制所有线程都按照相同的顺序(例如先锁A,再锁B)申请锁,即可打破循环等待。

工程实践建议:
- 在团队编码规范中明确锁的层级和获取顺序。
- 考虑使用
pthread_mutex_trylock配合重试机制来实现非阻塞或带超时的锁获取,提升系统健壮性。
- 在开发和测试阶段,积极使用死锁检测工具(如Valgrind的Helgrind工具、GCC的ThreadSanitizer)进行排查,这对于在资源受限的嵌入式平台上开发稳定软件尤为重要。
四、总结
掌握嵌入式Linux多线程编程,从“能跑”到“稳定可靠”,需要遵循三条核心原则:
- 最小化共享:优先考虑无共享架构(如消息传递),减少共享内存的使用。
- 最小化临界区:锁的范围应尽可能小,只保护必要的数据访问,而非计算过程。
- 统一加锁顺序:建立并遵守全局的锁获取顺序,从根本上预防死锁。