开始之前
你需要准备什么
荔枝派 4A
DHT22(IO1_6)、继电器/风扇(IO1_5)、外接 LED(IO1_4);MQTT Broker 可达。板端原生 gcc 或主机交叉工具链。
主机 gcc
跑 race-demo 时用本机 gcc -fsanitize=thread(ThreadSanitizer,检查谁在抢同一变量)。
代码
race-demo/ 丢更新;snapshot-lock/ 成对快照加锁;tri-thread/ 产品骨架。
本章你会学到什么
6.1 从单线程过渡到多线程
单线程按顺序做完三件事;等待网络时,采样和风扇判定排在返回之后。
6.2 三线:采集、控制、通信
三条线程各干什么、共享哪份状态。
6.3 共享状态与干净停
共享字段要加锁;Ctrl+C 后关风扇再退出。
从单线程过渡到多线程
单线程按顺序做三件事
本章板上要同时做三件事:按周期读 DHT22(第四章)、按阈值开/关风扇(第三、四章)、跑 MQTT 收发(第五章)。这三件事可以放在一个进程、一条线程里,用一个 while 按顺序做完:先处理网络,再读传感器,再判风扇。
第五章的 mqtt-led 在连上 Broker 之后调用 mosquitto_loop_forever。这个调用让执行流停留在套接字等待上。这次调用返回之后,同一条线程才执行排在后面的「读 DHT」「判风扇」。温度在第 0 秒越过 29°C 时,风扇开断要等这次等待返回之后才执行,间隔等于这次等待的时长。
把 mosquitto_loop_forever 换成带超时的 mosquitto_loop(例如每次最多等 50 毫秒就返回),返回后再读 DHT、再判风扇、再进入下一次 mosquitto_loop:仍然是单线程,三件事都能做完。第四章用 select 在一条线程里轮流处理「标准输入可读」和「采样周期到」,也是这一种顺序调度。读 DHT 若内部忙等约 30 毫秒,或两次采样之间 sleep(2),这段时间里执行流停留在采样或休眠上,已经到达的 MQTT 命令要等这段结束才被 mosquitto_loop 处理。灯可以晚 2 秒再亮;风扇按温度关断也会晚这同样一段时间。
本章用三条线程,让三件事按各自的周期进行:采集线程每 2 秒读 DHT22,把结果写入 last_temp;控制线程按自己的周期读 last_temp 和阈值,驱动继电器;通信线程调用 mosquitto_loop,在套接字上等待 MQTT。通信线程停留在等待时,采集线程仍然每 2 秒写入 last_temp,控制线程仍然按自己的周期拉继电器。温度在第 0 秒越过 29°C 时,控制线程仍可在约 200 毫秒量级去开风扇,开断时刻由控制线程自己的周期决定。这是本章要验收的行为。
采集线程写 last_temp,控制线程读同一个 last_temp。同一变量上一写一读会交错:控制线程可能读到一次尚未写完的值,日志里的温度和风扇开断对不上。对这一次写和这一次读加同一把互斥锁,控制线程读到的是一次完整写入之后的值。锁的写法在 6.3。两条线程若只访问各自的局部变量、地址不同,这些变量各自保持自己的值,这一对访问用不上这把锁。本课温控闭环把最新温度放在共享的 last_temp 里,所以实验里对这个字段加锁。
select 的关系:第四章在一条线程里用 select 轮流处理标准输入和采样周期。本章在同一进程里用三条线程,采集、风扇、MQTT 各一条小循环。两种都是课内要会的调度;本章验收三线程按各自周期运行,以及对 last_temp 的加锁读写。
while 如何按顺序做完三件事;能指出 mosquitto_loop_forever 等待期间采样和风扇判定排在返回之后;能说出三线程下通信等待时采集和控制仍按自己的周期运行;知道对同一个 last_temp 的写和读加同一把锁。
三线:采集、控制、通信
本节标题里的三线,完整说法是:采集线程、控制线程、通信线程。它们在同一进程里并行跑,通过一份共享状态交换数据。
职责拆分
图说明:采集只负责把最新温湿度写进共享状态;控制只负责读温度与阈值、驱动风扇;通信只负责 MQTT 下行改阈值/控灯与上行报状态。谁都不包办别人的活。
| 线程 | 做什么 | 不做什么 |
|---|---|---|
| 采集线程 | 按周期读传感器,更新 last_temp/last_hum | 不直接开风扇、不跑网络库 |
| 控制线程 | 读温度与阈值,调用风扇 GPIO | 不阻塞在 MQTT |
| 通信线程 | 跑 MQTT 循环;下行改阈值或灯;上行发布状态 | 不做 DHT 位带采样 |
骨架一览
chapters/ch06/code/tri-thread/ 给出 pthread 创建、共享结构、三线程函数空壳与信号退出标志。实验中补全各线程循环与加锁读写。退出标志请用 #include <stdatomic.h> 的 atomic_int(勿只写 volatile int)。
struct shared_state { pthread_mutex_t lock; /* 互斥锁:同一时刻只许一条线改关键字段 */ float last_temp; float t_high, t_low; int fan_on; atomic_int running; /* Ctrl+C 原子置 0;各线程 load 后退出(勿只用 volatile) */ };
$ cd chapters/ch06/code/tri-thread $ make clean && make $ file tri-thread $ scp tri-thread user@board-ip:~/
void *(*)(void *);创建后用 pthread_join 等其结束。风扇与 GPIO 初始化放在开线程之前,避免多线同时申请同一 line。
共享状态与干净停
锁与干净停
采集写温度、控制读温度、通信改阈值时,若两条线同时碰到同一字段,可能读到「写到一半」的值,或丢掉某次更新。用 互斥锁(mutex:同一时刻只许一条线程进入临界区)包住读写:
pthread_mutex_lock(&st->lock); temp = st->last_temp; high = st->t_high; pthread_mutex_unlock(&st->lock);
last_temp;其他线程在锁外等待干净停:注册 SIGINT(Ctrl+C)处理函数,用 atomic_store 把 running 置 0(信号与各线程并发读写退出标志时,volatile 不够,TSan 仍会报 race);各线程 atomic_load 判断后退出;主线程 join 完毕后关风扇(GPIO 写入失败要报错,勿误报已关)、释放 GPIO、断开 MQTT。验收时必须看到风扇停转再进程结束。
「同一 last_temp 上一写一读如何交错、加锁后如何读到完整写入」,完整步骤在下一节「深入理解」。
last_temp 的写和读加同一把锁 · 能口述干净停步骤 · 带着无锁交错的现象进入深入理解。
竞态:板上必现错乱,电脑上用 TSan 指出两行
本节把三个词说清楚:竞态(data race)是两条线程无同步地并发读写同一共享变量,结果不确定甚至写乱;互斥锁(mutex)保证同一时刻只有一条线程进入临界区;ThreadSanitizer(TSan)是编译加上 -fsanitize=thread 的检查器,能在运行时报告「哪两行在抢同一变量」。板上交叉工具链未必开 TSan,所以产品现象在荔枝派 4A 上看,机理用主机上的 race-demo 演示。
图说明:先看 [RACE] 必现,再加锁对照,再用 TSan 指出两行。
(1)(2) 荔枝派 4A:无锁错乱 → 加锁正确
原因:采集线程写共享温度,控制线程读同一份并写 fan_on,通信线程可能同时改 t_high。没有锁时,这些访问可以交错发生。
机制(无锁时):脚手架把同一次采样拆成 temp_a / temp_b 两步写,中间故意留空窗;控制线程读到两值不一致就打印 [RACE]。这是可稳定复现的证据。
机制(加锁后):成对写入落在同一把锁里,空窗消失。[RACE] 不再出现,阈值控温与 MQTT 改阈值恢复一致。
- 1板上先关锁
脚手架默认USE_LOCK 0,跑十几秒,日志应反复出现[RACE],退出时race_hits远大于 0。 - 2再开锁对照
改为USE_LOCK 1;[RACE]消失,race_hits=0,风扇按阈值动作,MQTT 下行仍正常。
图说明:锁保证这一次成对写完整结束;三线仍按各自周期运行,碰共享温度时排队。
(3) 电脑:race-demo + ThreadSanitizer 指出两行
chapters/ch06/code/race-demo/ 是故意让两条线程无锁抢同一计数器的小程序(无板、无 GPIO)。先无锁跑一遍,计数会偏小(丢更新);再用 TSan 编译运行,报告会打印发生数据竞争的两处源码行。
$ cd chapters/ch06/code/race-demo $ make $ ./race-demo # 无锁:最终计数明显小于期望(丢更新) $ sudo sysctl vm.mmap_rnd_bits=28 # 固定步骤:新内核地址随机化位数与 TSan 冲突,调低后 TSan 才能稳定跑通 $ make tsan $ ./race-demo # ThreadSanitizer 打印数据竞争报告,标出两处访问行号
保存终端中 TSan 指出的两行,对照源码:那就是「谁和谁在无同步地碰同一变量」。产品路径(tri-thread)验收以加锁版本为准。race-demo 只用于在电脑上保存 TSan 报告。
图说明:TSan 不替你「修」程序,它只把竞态从「感觉偶尔不对」变成「具体是这两行」。你回去加 mutex,再跑应不再报同一竞争。
USE_LOCK 0 时日志反复出现 [RACE];USE_LOCK 1 后 race_hits=0、风扇按阈值动作;race-demo 经 TSan 指出抢同一变量的两行。共享温度成对写入进互斥锁。」
sudo sysctl vm.mmap_rnd_bits=28 是本课固定步骤。