4. 线程同步基础:竞态条件与临界区、原子操作、内存屏障

好,咱们今天聊点硬核的。多线程编程里,最让人头疼的问题是什么?我个人觉得,就是「数据打架」。两个线程同时读写同一个变量,结果谁也不知道最终值是多少。这就是竞态条件。嗯,咱们一步步拆解。

4.1 竞态条件与临界区

先说说竞态条件。说白了,就是多个线程的执行顺序不确定,导致结果依赖于线程调度的「运气」。我在项目中遇到过好几次,明明逻辑看起来没问题,一跑多线程就出幺蛾子。查了半天,发现是竞态条件在作祟。

举个例子:两个线程同时对一个全局变量 countercounter++。你想想看,counter++ 在底层其实是三步操作:读取、加1、写回。如果线程A刚读完,线程B也读到了同样的值,然后各自加1写回——结果只加了1次,而不是2次。这就是典型的竞态条件。

核心概念:

  • 竞态条件:多个线程对共享数据的访问顺序不确定,导致结果不可预测。
  • 临界区:访问共享资源的代码片段。同一时刻只允许一个线程进入。

那怎么解决?很简单——保护临界区。让同一时刻只有一个线程能执行这段代码。鸿蒙里常用的手段有互斥锁、原子操作等。我建议你先把临界区画清楚,哪些代码是「敏感」的,然后才考虑用什么机制保护它。

4.2 原子操作

原子操作,听起来高大上,其实意思就是「不可分割的操作」。要么全部执行完,要么一点都没执行。不会出现执行到一半被中断的情况。

鸿蒙系统提供了丰富的原子操作API。我个人习惯用 atomic_fetch_add 这类函数,比用锁轻量得多。你想想看,如果只是对一个整数做加减,用锁就有点杀鸡用牛刀了。

// 鸿蒙原子操作示例
#include <atomic.h>

int counter = 0;

// 原子加1
atomic_fetch_add(&counter, 1);

// 原子减1
atomic_fetch_sub(&counter, 1);

// 原子比较并交换
int expected = 0;
int desired = 1;
atomic_compare_exchange_strong(&counter, &expected, desired);

我曾经在做一个高性能日志系统时,就用原子操作来维护一个全局的日志序号。几十个线程同时写日志,用原子操作完全扛得住,性能比用锁高了一个数量级。

小提示:原子操作只适合简单的读写场景。如果你的临界区逻辑复杂(比如需要同时更新多个变量),还是老老实实用锁吧。

4.3 内存屏障

内存屏障,这玩意儿很多人觉得玄乎。其实没那么复杂。现代CPU为了性能,会乱序执行指令。单线程下没问题,但多线程下就麻烦了——你看到的「顺序」可能不是实际执行的顺序。

举个例子:线程A先写了一个flag,再写data。线程B看到flag变了,就去读data。但如果没有内存屏障,CPU可能先把flag写入了内存,data还在缓存里。线程B读到flag变了,但data还是旧值。这就出问题了。

鸿蒙里常用的内存屏障有几种:

屏障类型 作用
atomic_thread_fence 线程间的内存顺序保证
atomic_signal_fence 同一线程内信号处理时的顺序保证
__sync_synchronize GCC内置的全屏障
// 内存屏障示例
int flag = 0;
int data = 0;

// 线程A
data = 42;
atomic_store_release(&flag, 1);  // 释放语义:保证data写入在flag之前

// 线程B
while (atomic_load_acquire(&flag) == 0);  // 获取语义:保证flag读取在data之前
// 此时可以安全读取data
int value = data;

嗯,这里要注意:内存屏障不是银弹。滥用内存屏障反而会降低性能。我建议只在确实需要保证内存顺序的地方使用,比如实现无锁数据结构时。

避坑指南:我曾经在一个项目中,为了「优化性能」,把锁换成了原子操作加内存屏障。结果代码变得极其复杂,还出现了几个隐蔽的bug。后来我学乖了——能用锁的地方,别瞎折腾。原子操作和内存屏障是给高手用的,新手先用好锁再说。

4.4 三者关系

最后总结一下这三者的关系:

  • 竞态条件是问题本身,临界区是问题区域。
  • 原子操作解决简单的竞态条件,比如计数器、标志位。
  • 内存屏障解决内存顺序问题,保证多线程看到的数据是一致的。

在实际开发中,我建议你按这个顺序思考:

  1. 先识别竞态条件,划定临界区。
  2. 如果临界区只是简单的读写,用原子操作。
  3. 如果需要保证内存顺序,加内存屏障。
  4. 如果逻辑复杂,直接用锁(后面章节会讲)。

好了,这一章就到这里。下一章咱们聊聊鸿蒙里的各种锁——互斥锁、读写锁、自旋锁,看看它们各自适合什么场景。