一、并发的本质
1. 并发≠并行
首先,我们需要理解两个核心概念:
- 并发(Concurrency):多个任务在时间片上交替执行,宏观上“同时”进行,微观上则是串行切换。
- 并行(Parallelism):多个任务在多个CPU核心上真正地同时执行。
在单核ARM等嵌入式芯片上,你编写的多线程代码实现的是并发;而在多核处理器上,才有可能实现并行。但无论是并发还是并行,只要存在对共享资源的访问,就必须面对竞态条件(Race Condition)的挑战。关于更基础的并发与网络原理,可以参考网络/系统相关内容。
2. 竞态的根源
竞态条件的根源在于多个执行流对共享资源的访问顺序具有不确定性。解决这一问题,是保证多线程程序稳定性的核心。这种不确定性主要源于以下几个方面:
(1)一行C代码≠一条CPU指令
看一个常见的计数器自增操作:

看似简单的 counter++ 操作,在编译后实际可能对应多条机器指令:
LOAD counter → 寄存器
ADD 寄存器 + 1
STORE 寄存器 → counter
如果两个线程各执行10万次此操作,理论结果应为20万。但实际运行结果往往在13万到18万之间波动。原因就在于两个线程的指令序列可能发生“交叉执行”,导致部分累加操作丢失。
(2)编译器和CPU的优化
为了提升性能,编译器可能会对指令进行重排序,现代CPU也普遍采用乱序执行技术。这意味着我们编写的代码顺序,并不一定是处理器实际执行的顺序。
(3)多核CPU的缓存一致性
每个CPU核心通常拥有自己的L1/L2缓存。一个核心对某内存地址的修改,不会立即对其他核心可见,这就是缓存一致性问题。
二、POSIX线程库三大同步原语
POSIX线程(pthread)库提供了几种关键的同步机制来应对上述挑战,主要包括互斥锁、条件变量和读写锁。
2.1 互斥锁
互斥锁的语义非常直观:在同一时刻,只允许一个线程持有该锁。

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

正确做法:仅锁定共享数据访问
应当只对访问共享数据的代码段加锁,而将无需共享的计算逻辑放在锁外执行。

2.2 条件变量
在典型的“生产者-消费者”模型中,消费者线程如何高效地获知“已有数据可处理”?使用条件变量是解决这类线程等待与通知场景的最佳实践——它能让线程在条件不满足时主动休眠以节省CPU,在条件满足时被精准唤醒。
低效方案:忙等待(轮询)

高效方案:使用条件变量
使用条件变量的标准范式如下:
等待方(消费者)步骤:
- 加互斥锁 (
pthread_mutex_lock)。
- 使用
while循环检查条件是否满足(例如 while(queue.empty()))。
- 条件不满足时,调用
pthread_cond_wait 进入休眠(该函数会原子性地释放互斥锁并等待)。
- 被唤醒后,重新检查条件(防止虚假唤醒),条件满足则执行业务逻辑。
- 解锁互斥锁 (
pthread_mutex_unlock)。
通知方(生产者)步骤:
- 加互斥锁 (
pthread_mutex_lock)。
- 修改条件(如向队列添加数据)。
- 发送通知 (
pthread_cond_signal 唤醒一个等待线程,或 pthread_cond_broadcast 唤醒所有等待线程)。
- 解锁互斥锁 (
pthread_mutex_unlock)。
示例代码:

为什么必须用while循环检查条件,而不能用if?
这是因为存在“虚假唤醒”(spurious wakeup)的可能——即线程可能在未收到任何通知信号的情况下从 pthread_cond_wait 中返回。这是POSIX标准允许的行为,使用 while 循环可以确保被唤醒后再次验证条件是否真正满足。
2.3 读写锁
如果你的应用场景是“读多写少”(例如90%的操作为读取,10%为写入),使用互斥锁会造成不必要的性能浪费,因为读操作之间本身并不需要互斥。

读写锁允许多个线程同时持有“读锁”进行读取,但“写锁”是独占的。当有线程持有写锁时,其他所有读写线程都会被阻塞。
下图总结了三种同步原语的典型适用场景:

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

以上程序运行后会卡死,且CPU占用率极低。这是典型的AB-BA死锁模式:两个线程以相反的顺序请求两把锁,在某种时序下会互相等待对方释放锁,从而陷入永久阻塞。
发生死锁的时序图:

修复方案:统一加锁顺序
强制所有线程都按照先锁A、后锁B的顺序申请锁,即可避免循环等待。

工程实践建议:
- 在团队编码规范中明确锁的层级和获取顺序。
- 考虑使用
pthread_mutex_trylock 配合超时机制,避免长时间阻塞。
- 在开发测试阶段,积极使用线程死锁检测工具,如 Helgrind、ThreadSanitizer (TSan) 等。
四、总结
编写稳定可靠的多线程嵌入式程序,可以遵循以下三条核心原则:
- 最小化共享:优先考虑无共享架构(如Actor模型),能通过消息传递通信的,就不使用共享内存。
- 最小化临界区:锁的范围应尽可能小,仅保护对共享数据的访问,而非耗时的计算过程。
- 统一加锁顺序:建立全局的锁获取顺序规则,这是从设计上预防死锁的最有效手段。