跳转到内容
新建笔记

Qt 线程选型:工作对象、线程池与任务通知

Qt 提供专用线程、工作对象和线程池等不同层次的工具。先判断任务是否需要持续运行的事件循环、是否需要返回值,再选择接口;“把某个对象放进线程”并不等于任意成员调用都会自动切换线程。

本组示例面向 Qt 6、C++17;完整程序已在 Qt 6.10.2 上验证。

方式适合的任务需要明确的边界
继承 QThread、重写 run()单次计算、独立循环或阻塞任务自定义 run 默认没有事件循环,退出由任务本身控制
QObject 工作对象、moveToThread()有槽、定时器或异步网络事件的长期对象使用队列调用;对象不能有阻止移动的 parent
QRunnable 与 QThreadPool一批可独立完成的任务,复用线程定义任务寿命,不把工作线程当作长期 QObject 所属线程
QtConcurrent函数调用、映射归约、需要 future 的任务处理数据寿命、结果通知、取消能力和异常

步骤与完整例子分别见 QThread 基础、Worker Object 和 QtConcurrent。不要再复制四套相同的布尔循环来展示不同 API。

操作含义
start()启动所管理的线程,并在该线程执行 run
started() / finished()线程启动和结束通知,不等同于业务中的每一项任务开始/完成
isRunning() / isFinished()查询线程运行状态,不能替代对共享业务数据的同步
quit() / exit(code)请求线程事件循环退出;不会强制打断正在执行的函数
requestInterruption()设置协作中断请求,任务须主动检查
wait()阻塞调用者直到线程结束;通常在最后回收阶段使用
currentThread() / currentThreadId()查询当前执行代码的线程,不是某个对象的线程归属
idealThreadCount()可用于估计并行度的建议值,不是始终固定的物理核心数
sleep() / msleep() / usleep()阻塞当前线程,不是异步定时,也不保证精确唤醒时间

QThread 对象通常仍属于创建它的线程,run 才在新线程执行。给 QThread 子类增加普通槽,再从主线程以普通函数方式调用,并不会自动在其管理的线程运行。QThread 官方说明

一个线程池任务的完整例子

跳转到“一个线程池任务的完整例子”
// main.cpp;链接 Qt6::Core
#include <QCoreApplication>
#include <QRunnable>
#include <QThreadPool>
#include <atomic>
#include <cassert>
class AddTask final : public QRunnable {
public:
AddTask(std::atomic<int> &total, int amount)
: total_(total), amount_(amount) {}
void run() override { total_.fetch_add(amount_); }
private:
std::atomic<int> &total_;
int amount_;
};
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
std::atomic<int> total{0};
QThreadPool pool;
pool.setMaxThreadCount(2);
pool.start(new AddTask(total, 3));
pool.start(new AddTask(total, 4));
pool.waitForDone(); // 命令行检查允许阻塞;GUI 正常操作期间不这样等待
assert(total.load() == 7);
}

QRunnable 默认允许线程池自动删除任务,因此这里每次提交新分配的任务,不把栈上 QRunnable 交给这种自动删除机制。引用的 total 必须活到两个任务都结束;waitForDone() 建立了本例的回收边界。QThreadPool

QThreadPool::globalInstance() 获取全局池;maxThreadCount() 查询配置上限,setMaxThreadCount() 修改上限,activeThreadCount() 查询当前活动数量。局部独立池可将一类任务与全局任务分开。QtConcurrent 默认也使用全局池,池忙时提交的工作可能排队,不能假设提交后立即开始。

优先级与停止不是一回事

跳转到“优先级与停止不是一回事”

优先级包含 IdlePriority、LowestPriority、LowPriority、NormalPriority、HighPriority、HighestPriority、TimeCriticalPriority 和 InheritPriority。默认启动值 InheritPriority 继承创建线程的优先级;实际调度还受平台和策略限制。提高优先级不能修复死锁、数据竞争或阻塞 GUI 的设计。

普通 bool 被一个线程写、另一个线程无同步地读,会产生数据竞争;停止标志应使用明确的同步机制,或使用 QThread 的协作中断接口。quit() 只处理事件循环,不会让一个无限计算循环自行结束。terminate() 可能在持锁或修改资源时强制终止执行,不应作为常规停止方式。

后台线程不要直接操作 QWidget 或 QPixmap 界面对象。图像计算通常传递 QImage、数值或其他有明确寿命的数据,结果通过主线程的接收者更新显示。Qt 线程与对象

完成、进度和失败如何通知

跳转到“完成、进度和失败如何通知”
需求可用接口使用条件
单个异步函数结果与异常QFuture 的 result / resultAt未就绪时可能阻塞;在完成回调中提取并处理异常
GUI 中接收任务完成与进度QFutureWatcher 的 finished、progressValueChanged、resultReadyAt先连接再 setFuture;watcher 所在线程须处理事件;任务本身须提供进度
一组线程池任务最终回收QThreadPool::waitForDone会阻塞调用线程;一般用于关闭或命令行程序回收阶段
自定义线程业务状态自定义成功、错误、进度信号线程结束仅说明执行结束,业务成功须单独表达

QFuture::isValid() 表示结果是否可访问,不是错误检测接口;finished 也不等于成功。QtConcurrent 任务异常可在结果提取时重新抛出,调用处需要处理;自定义 QThread/QRunnable 任务应在线程入口内部处理异常,再明确报告错误,不能让异常越过线程入口或 Qt 事件回调。详细例子见 QtConcurrent 与 完成通知和请求寿命。QFuture、QFutureWatcher

如果给 QRunnable 同时混入 QObject 来发完成信号,信号发出时 run() 可能还没返回。线程池自动删除和其他线程的 deleteLater() 不能同时负责同一任务;不要把“收到业务完成信号”直接当作“现在可以从另一线程销毁 runnable”。先明确自动删除策略、QObject 线程归属和任务返回后的回收者。

其他并发工具与实际场景

跳转到“其他并发工具与实际场景”
场景入口与判断
简单 CPU 计算、独立的阻塞文件读取QtConcurrent::run 或 QRunnable;传入数据副本/稳定路径,定义资源寿命
批量映射、筛选、归约QtConcurrent 的 mapped、filtered、mappedReduced 等;输入集合须满足相应接口的寿命要求
长期网络对象、定时器、事件驱动状态机Worker Object 与事件循环;异步网络 API 本身也可在主线程工作,无需机械地另开线程
定期刷新 GUI 状态GUI 线程的 QTimer;timer 不创建后台线程,回调不能执行长时间阻塞操作
共享数据、读多写少或资源许可QMutex、QReadWriteLock、QSemaphore,按完整访问协议选型
生产者消费者QMutex 配合 QWaitCondition,等待循环检查状态并有关闭协议
独立计数QAtomicInt 或 std::atomic;复合检查/更新和关联数据另行同步
在指定 QObject 所在线程执行操作带上下文的队列信号或 QMetaObject::invokeMethod;接收对象有效且线程有事件循环

有界队列同时等待“非空”和“未满”时,使用不同等待条件更容易确保唤醒正确的等待者;若让生产者和消费者共用一个条件并只 wakeOne(),可能唤醒不能继续工作的同类线程。等待条件和关闭示例见 线程同步。

AutoConnection 依据发射信号时的当前线程与接收上下文的线程归属决定直接还是排队调用,不能只看 sender 对象属于哪个线程。DirectConnection 在发射线程立即调用;QueuedConnection 在接收线程的事件循环处理。连接机制不会自动保护槽内部共享容器,也不能消除 BlockingQueuedConnection 的死锁风险。连接管理

继承 QThread 是 Qt 支持的模式,不应被笼统称为违反对象模型。线程池能降低重复创建线程的开销,但吞吐、内存和延迟取决于任务大小、I/O、锁竞争与队列长度;不能用固定星级断言某一种方式总是最快或完全安全。按测量选择并行度,线程数量也不必固定等于 CPU 核数。