3. 进程、线程与并发基础
在 Qt 嵌入式多线程应用工程中,理解进程与线程的区别、并发与并行的差异、上下文切换的代价以及线程生命周期,是写出稳定、高效、可维护多线程程序的前提。很多初学者把“多线程”等同于“更快”,结果在嵌入式设备上遇到界面卡顿、数据错乱、CPU 占用飙升等问题,根源往往是对这些基础概念理解不深。本节从操作系统视角出发,结合 Qt 的实际编程模型,把这些概念讲清楚。
一、进程与线程的本质区别
进程是操作系统进行资源分配的基本单位,它拥有独立的虚拟地址空间、文件描述符表、信号处理表等资源。线程是 CPU 调度的基本单位,同一进程内的多个线程共享该进程的地址空间和大部分资源,但各自拥有独立的栈、寄存器上下文和线程局部存储。
在嵌入式 Linux 上,创建一个进程通常使用 fork() 或 posix_spawn(),代价较高,因为需要复制页表、建立新的地址空间;而创建一个线程使用 pthread_create(),代价小得多。Qt 中对应的是 QProcess 与 QThread:QProcess 用于启动外部程序,进程间通过管道、套接字通信;QThread 用于在同一进程内并发执行任务,线程间可直接共享内存,但必须配合同步机制。
二、并发与并行:不是一回事
并发(Concurrency)指多个任务在逻辑上同时推进,宏观上“同时”,微观上可能是交替执行;并行(Parallelism)指多个任务在物理上真正同时执行,需要多核 CPU 支持。单核 CPU 上只能并发,不能并行;多核 CPU 上既可以并发也可以并行。
在 Qt 中,QThread、QtConcurrent、QThreadPool 提供的是并发能力,是否真正并行取决于硬件核心数和调度器。嵌入式设备常见的是双核或四核 ARM,如果线程划分合理,采集线程和显示线程可以真正并行,从而避免界面卡顿。
三、上下文切换:看不见的性能开销
上下文切换是操作系统在切换执行线程时保存当前线程的寄存器、程序计数器、栈指针等状态,并恢复下一个线程状态的过程。它本身不执行用户任务,却消耗 CPU 时间。在嵌入式设备上,频繁切换会带来明显开销,尤其是当线程数量过多、锁竞争激烈或时间片过短时。
上下文切换的主要来源包括:时间片耗尽、阻塞式系统调用、等待互斥锁、条件变量唤醒、中断处理等。Qt 中常见的误区是“把任务拆得越细越好”,结果创建了几十个线程,每个线程都在等锁,CPU 大量时间花在切换上,吞吐量反而下降。
# 在嵌入式 Linux 上观察上下文切换
$ pidstat -w -p <pid> 1
Average: UID PID cswch/s nvcswch/s Command
Average: 1000 812 320.5 45.2 sensor_app
# cswch/s:自愿切换(等待资源)
# nvcswch/s:非自愿切换(时间片耗尽)
四、线程生命周期与 Qt 中的状态变化
线程生命周期通常包括:新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)、终止(Terminated)。在 Qt 中,QThread 的状态通过 isRunning()、isFinished() 查询,启动用 start(),请求退出用 quit() 或 exit(),等待结束用 wait()。
推荐做法是“工作对象 + moveToThread”模式:把业务逻辑封装到 QObject 子类,通过 moveToThread() 移入线程,用信号槽跨线程通信。这样线程的事件循环负责调度,避免直接继承 QThread 并重写 run() 带来的混乱。
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork() {
while (!m_stop) {
// 采集、计算、写日志
}
emit finished();
}
void stop() { m_stop = true; }
private:
std::atomic<bool> m_stop{false};
};
QThread thread;
Worker* worker = new Worker;
worker->moveToThread(&thread);
connect(&thread, &QThread::started, worker, &Worker::doWork);
connect(worker, &Worker::finished, &thread, &QThread::quit);
thread.start();
// 退出时
worker->stop();
thread.wait(3000);
五、核心概念对照清单
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源分配单位 | 是 | 否,共享进程资源 |
| 调度单位 | 否 | 是 |
| 地址空间 | 独立 | 共享 |
| 创建/切换开销 | 大 | 小 |
| 通信方式 | 管道、共享内存、套接字 | 共享变量、信号槽、互斥锁 |
| 崩溃影响 | 通常不影响其他进程 | 可能导致整个进程退出 |
| Qt 对应类 | QProcess | QThread、QtConcurrent |
六、案例与常见误区复盘
某嵌入式数据采集项目最初把串口读取、协议解析、界面刷新都放在主线程,界面在数据量大时明显卡顿。改造后:串口读取放入独立线程,解析放入 QThreadPool,界面只负责显示,通过信号槽传递结果。改造后界面流畅,CPU 占用从 85% 降到 45%。
常见误区包括:一是认为线程越多越快,忽视上下文切换成本;二是直接在子线程操作 UI,导致 Qt 报错或崩溃;三是用 terminate() 强制结束线程,造成资源泄漏;四是共享数据不加锁,出现偶发数据错乱。正确做法是:合理划分线程数量,UI 只在主线程操作,线程退出使用标志位加 wait(),共享数据用 QMutex、QReadWriteLock 或原子变量保护。