1. 课程导入
嵌入式设备一旦接入触摸屏、传感器、网络通信和本地存储,单线程程序很快就会暴露问题:界面卡顿、数据丢包、响应延迟、看门狗复位。Qt 提供了跨平台的 GUI 与系统抽象能力,而多线程则决定了这套应用能否在资源受限的 ARM 平台上稳定运行。本课程的目标,不是孤立地讲 QThread 的 API,而是把 Qt 的事件循环、线程模型、嵌入式 Linux 的调度机制和真实工程结构串成一条可落地的开发链路。
一、学习目标与能力地图
完成本课程后,你应当具备四项可验证的能力:第一,能解释 Qt 事件驱动模型与线程亲和性(thread affinity)的关系,判断一个对象该属于哪个线程;第二,能根据任务类型选择 QThread 子类化、moveToThread、QtConcurrent 或线程池,而不是一律开线程;第三,能设计线程间的数据传递与同步方案,正确使用信号槽跨线程连接、互斥锁、条件变量和原子操作;第四,能在嵌入式目标板上完成交叉编译、部署、性能观测与死锁排查。
二、核心概念:先厘清五个词
- 事件循环(Event Loop):
QCoreApplication::exec()启动的消息分发机制,负责把事件投递到目标对象所在线程并调用其事件处理函数。 - 线程亲和性:每个
QObject属于创建它的线程,其槽函数默认在该线程执行;跨线程调用会被转换为事件投递。 - 信号槽跨线程连接:当发送者与接收者不在同一线程时,默认使用
Qt::QueuedConnection,参数需可被元类型系统复制。 - GUI 线程唯一性:所有界面控件只能在主线程创建和访问,工作线程通过信号把结果交回主线程刷新。
- 资源竞争:多个线程同时读写共享数据时产生的不可预期结果,需要锁、原子量或无共享设计来消除。
这五个概念构成了后续所有章节的判断依据。例如,当你在工作线程中直接调用 ui->label->setText(),程序可能不崩溃,但行为不可靠——这正是线程亲和性被破坏的典型表现。
三、整体框架:从应用到目标板
本课程的知识结构可以分为四层:应用层负责界面与业务逻辑;并发层负责线程划分、任务调度与同步;Qt 框架层提供事件循环、信号槽、容器与网络模块;系统层则是嵌入式 Linux 的进程、线程、调度与驱动接口。四层之间的关系如下图所示。
理解这张分层图的关键在于:上层的问题往往要在下层找原因。界面卡顿可能不是 Qt 的问题,而是工作线程优先级过低或锁竞争过长;信号槽没有触发,可能是接收对象所在线程的事件循环没有运行。
四、典型场景:一个采集与显示并存的设备
假设设备需要以 100 Hz 采集串口传感器数据,同时在触摸屏上实时绘制曲线,并把异常数据写入本地日志。若全部放在主线程,串口读取和绘图会互相阻塞,界面刷新率骤降。合理的划分是:主线程负责界面与用户输入;采集线程负责串口读取与解析;日志线程负责写文件;绘图数据通过信号槽以队列方式交给主线程。这样既保证了 GUI 的响应性,又避免了多线程直接操作界面。
| 任务 | 所属线程 | 通信方式 | 注意事项 |
|---|---|---|---|
| 界面刷新与触摸响应 | 主线程 | 直接调用 | 禁止阻塞操作 |
| 串口采集与协议解析 | 采集线程 | 信号槽(队列) | 控制采集周期抖动 |
| 曲线数据缓存 | 采集线程 / 主线程 | 互斥锁保护环形缓冲 | 锁粒度尽量小 |
| 日志落盘 | 日志线程 | 条件变量 + 队列 | 避免频繁 fsync |
五、开发环境搭建
课程以 Ubuntu 主机 + ARM64 嵌入式 Linux 目标板为例。主机安装 Qt 开发环境与交叉编译工具链,目标板运行带 Qt 运行库的根文件系统。建议使用 Qt 5.15 LTS 或 Qt 6.x,二者在线程 API 上差异不大,但 Qt 6 对元类型和并发库有调整。
# 主机安装基础工具
sudo apt install build-essential cmake ninja-build git
# 安装交叉编译工具链(示例:aarch64)
sudo apt install g++-aarch64-linux-gnu
# 验证 Qt 版本与 qmake 路径
qmake -v
# 输出示例:QMake version 3.1 Using Qt version 5.15.2
# 交叉编译一个最小 Qt 工程
mkdir build-arm64 && cd build-arm64
cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain-aarch64.cmake
make -j4
QCoreApplication 与一个工作线程的最小程序,确认 Qt 运行库、字体、触摸设备和串口权限都正常,再进入正式功能开发。这样能把环境问题与代码问题分开定位。
六、常见误区与复盘
- 误区一:线程越多越快。线程切换和锁竞争会消耗 CPU,嵌入式平台核心数有限,线程数量应与任务类型和核心数匹配。
- 误区二:跨线程直接调用对象方法。未考虑线程亲和性,可能导致崩溃或数据错乱,应通过信号槽或显式同步。
- 误区三:忽略事件循环。在子线程中使用需要事件循环的类(如
QTcpSocket)却不调用exec(),会导致信号无法送达。 - 误区四:用
terminate()结束线程。强制终止可能让互斥锁处于锁定状态,正确做法是设置退出标志并等待线程自然结束。
helgrind、ThreadSanitizer 或 Qt 的日志类别,配合目标板上的 top -H 观察线程级 CPU 占用,才能稳定复现和定位问题。
本章建立了后续学习的坐标系:事件循环是基础,线程亲和性是判断依据,分层框架是设计地图,环境搭建是动手起点。接下来每一章都会围绕这条主线展开,把概念逐步落到可编译、可部署、可观测的工程代码上。