ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Qt多线程实战:从串口采集到FFT频谱显示的完整方案

Qt多线程实战:从串口采集到FFT频谱显示的完整方案 简介QT线程及多线程是一份面向Qt开发者的并发编程实战资源内容覆盖QThread线程创建与生命周期管理、信号槽跨线程通信机制以及QMutex、QWaitCondition、QSemaphore等同步原语的用法适合需要提升异步任务处理能力的C/Qt中初级开发者。资源包内共13个文件包含cpp源文件、h头文件、pro工程文件、ui界面文件及jpg/gif演示素材并配有readme说明文档便于对照源码逐步理解线程安全写法与常见陷阱。包体仅2.16MB轻量易用已有988人学习下载。代码案例从QThread子类重写run()到使用moveToThread()将工作对象迁移至新线程完整展示了正确执行后台任务并优雅退出的思路同时通过互斥锁性能对比图直观揭示锁开销与并发设计要点可帮助读者快速落地到网络通信、数据库操作等实际场景。1. 项目概述与真实需求拆解1.1 为什么Qt多线程总是一学就会、一写就废先说个实际场景你用QSerialPort做了一个串口调试助手波特率115200数据量一大界面就开始卡顿鼠标转圈拖动窗口像幻灯片。这时候几乎所有教程都会告诉你“用多线程啊”但等你真把数据读取放到QThread里问题反而更多了——要么界面还是卡要么程序直接崩溃更诡异的是明明线程退出了程序却关不掉。这个项目标题是“QT线程及多线程”但我更愿意把它理解成“如何正确地在Qt里使用多线程而不把自己绕进去”。Qt的多线程和原生C多线程最大的区别在于Qt不仅有线程本身还有一套事件循环、信号槽跨线程投递机制以及海量的“只能在主线程操作”的界面类。把这套机制理解透了多线程只是顺手的事理解不透就会陷入“加线程→更卡→再加线程→崩溃”的死循环。这篇内容适合三类人一是刚接触Qt、被界面卡顿问题逼着学多线程的初学者二是已经会用QThread但搞不清moveToThread和继承QThread到底该选谁的进阶者三是找工作前想系统梳理多线程知识点的求职者。我把这些年踩过的坑、验证过的方案、以及面试里经常被问到的细节都整理出来一次讲透。1.2 从热搜词看大家真正在困扰什么我翻了下相关搜索记录发现几个非常典型的高频问题qt崩溃、qt界面设计、qt怎么调用halcon、qt 串口编程、qt qcustomplot kissfft时域到频域波形、qt模拟鼠标点击事件还有qt打包应用程序 windeployqt。把这些关键词串起来能明显看出一个共性需求大家都在做“带界面的数据采集/处理工具”而且采集端和处理端往往涉及耗时操作。举个最典型的链路串口或者网络把数据收进来 → 用QCustomPlot画实时曲线 → 需要把时域信号用KISSFFT转成频域波形 → 最后还要打包发布给同事用。这个链路里的每一个环节只要数据量稍微大一点单线程必然卡顿。而解决这些问题核心路径只有一条把“数据采集”“数据处理”“界面绘制”三个环节解耦该并行的并行该回主线程的回主线程。所以这篇内容不会只讲理论我会把一个完整的“串口采集FFT频谱显示”案例拆开揉碎从线程方案选型、信号槽连接机制、到什么操作必须回主线程、以及最终打包时可能遇到的坑全部过一遍。看完你就能直接照着写自己的版本。2. Qt多线程的四种套路与选型思路2.1 继承QThread最经典也最容易用错的方案继承QThread重写run()函数是很多Qt教程喜欢教的第一个多线程示例。我自己最早学的时候也是这么做的代码大概长这样class WorkerThread : public QThread { Q_OBJECT protected: void run() override { // 耗时操作 QThread::msleep(5000); emit resultReady(done); } signals: void resultReady(const QString result); };然后在外层通过new一个WorkerThread、connect信号、调用start()来使用。这个方案的问题在于很多人把“继承QThread”理解成了“在QThread里跑业务逻辑”甚至把业务类的成员函数直接写在QThread子类里导致业务逻辑和线程生命周期强耦合项目一复杂就难以维护。关于这个方案Qt官方文档其实有过明确表态继承QThread更合理的使用方式是把QThread当作线程控制器而不是在线程里塞满业务。但官方归官方现实里大量老项目就是这么写的而且在小工具类项目里它确实够用。我的建议是如果是自用的临时脚本、几十一百行的工具类程序继承QThread没问题但如果是正经项目建议优先考虑后面几种方案。2.2 moveToThread官方推荐的业务与线程分离方案moveToThread的核心思想是把一个普通的QObject对象“挪”到某个线程里让对象的槽函数在自己所属线程里执行。这种方式下业务代码不需要继承任何线程类只关心自己的业务逻辑线程生命周期由外部的QThread对象管理。class Worker : public QObject { Q_OBJECT public slots: void doWork() { // 耗时操作在Worker对象所属线程执行 QThread::msleep(3000); emit workFinished(finished); } signals: void workFinished(const QString msg); }; // 主线程中 QThread *thread new QThread; Worker *worker new Worker; worker-moveToThread(thread); connect(thread, QThread::finished, worker, QObject::deleteLater); connect(this, MainWindow::startWork, worker, Worker::doWork); connect(worker, Worker::workFinished, this, MainWindow::onWorkFinished); thread-start();这个方案的关键理解点在于当你用一个信号去触发worker的doWork槽函数时连接方式如果是AutoConnection跨线程就会自动转成QueuedConnection槽函数的执行就会被投递到目标线程的事件循环里去执行。这相当于Qt帮你做了一个线程间的函数调用分发。我强烈建议正式项目里优先走这条路因为Worker可以独立测试、可以在不同线程间迁移、可以任意扩展信号槽接口代码的清晰度和可维护性都比继承QThread高一个档次。2.3 QtConcurrent一句话开启异步任务的轻量方案QtConcurrent适用于“我只需要跑一个耗时的函数跑完把结果给我”的简单场景不需要保留常驻线程时这是四个方案里写起来最爽的。#include QtConcurrent/QtConcurrentRun #include QFutureWatcher QFutureWatcherQString *watcher new QFutureWatcherQString(this); connect(watcher, QFutureWatcherQString::finished, this, [this, watcher](){ QString result watcher-result(); ui-label-setText(result); watcher-deleteLater(); }); QFutureQString future QtConcurrent::run([]() - QString { QThread::msleep(3000); return QStringLiteral(计算完成); }); watcher-setFuture(future);这里有个细节值得注意QFutureWatcher必须在主线程里创建它内部的finished信号会和主线程的事件循环自动关联所以lambda里可以直接更新界面。如果你不想用lambda也可以connect到一个普通的槽函数只要保证槽函数在主线程里执行就行。QtConcurrent底层走的是全局线程池默认线程数等于CPU核心数所以不要在里面跑死循环或者长期阻塞任务否则会占满线程池影响其他任务。需要长期占用的任务还是用moveToThread方案更合适。2.4 QThreadPool配合QRunnable面向大批量短任务的线程池方案如果是“成千上万个短小的任务每个任务耗时几百毫秒希望并发执行又不想手动创建线程”QThreadPool QRunnable的组合是正解。QRunnable和QObject没有关系不能直接使用信号槽通常需要自己写一个继承QRunnable的类并在里面持有结果回调。class CaculateTask : public QRunnable { public: CaculateTask(int start, int end, std::functionvoid(int) onFinished) : m_start(start), m_end(end), m_onFinished(onFinished) {} void run() override { int sum 0; for (int i m_start; i m_end; i) sum i; if (m_onFinished) m_onFinished(sum); } private: int m_start, m_end; std::functionvoid(int) m_onFinished; }; // 使用 QThreadPool::globalInstance()-start(new CaculateTask(1, 100000, [](int result){ QMetaObject::invokeMethod(this, []() { ui-label-setText(QString::number(result)); }, Qt::QueuedConnection); }));注意lambda里更新界面时用到了QMetaObject::invokeMethod配合Qt::QueuedConnection这是因为QRunnable的run()是在线程池工作线程里执行的不能直接操作UI需要投递回主线程。这个细节特别容易踩坑后面讲安全问题时会展开说。2.5 一张表看清四种方案怎么选方案适用场景优点缺点代码量继承QThread自用脚本、快速原型、简单定时任务简单直接认知门槛低业务与线程耦合难维护少moveToThread常驻业务对象、串口/网络通信类、需要长期运行的采集任务业务独立生命周期清晰官方推荐需要理解事件循环和连接机制中QtConcurrent::run一次性耗时计算、文件读写、耗时算法一句话启动异步写法最简不适合长期占用线程无法精细化控制线程少QThreadPoolQRunnable大批量短任务并发如批量图片压缩、多段数据块同时处理复用线程节省开销并发可控无法直接用信号槽结果回传稍繁琐中3. 线程安全与信号槽机制深度拆解3.1 为什么“跨线程更新UI”一定会出问题很多初学者会写这样的代码在子线程里直接调用ui-label-setText()结果程序没崩但界面偶尔闪烁、偶尔更新不及时甚至过一段时间突然崩溃。原因在于Qt的UI类几乎都不是线程安全的它们依赖主线程的事件循环来维护绘制状态。你在子线程里强行调用就相当于两个人同时往一个杯子里倒水可能洒出来也可能杯子直接碎。Qt的解决办法是信号槽的跨线程队列投递机制。发送方在某线程发出信号如果接收方在另一个线程且连接方式为AutoConnectionQt会自动判断需要转成QueuedConnection将调用事件投递到接收方所在线程的事件循环里排队执行。所以“更新UI”这个动作本身没有做错错的是直接调用正确的做法是让信号槽机制帮你把调用“投递”回主线程。这里我再推荐一个硬核兜底方法不论身处哪个线程、不管用什么框架只要你想安全地调用主线程里的某个函数都可以用QMetaObject::invokeMethod指定Qt::QueuedConnection。这个方法不依赖信号槽也能用特别适合QRunnable这类没有信号能力的场景。3.2 搞清楚AutoConnection与QueuedConnection的判定规则连接方式的判定逻辑其实很简单connect发生的时刻QObject的thread()返回哪个线程接收对象就归属于哪个线程。发送信号时如果发送方所在线程和接收方所在线程不同AutoConnection自动变成QueuedConnection相同则是DirectConnection槽函数在发送方线程里直接同步执行。这个判定规则有一个常见误用场景你把一个QObject对象通过moveToThread移动到工作线程后又想在主线程里直接connect它的信号此时如果接收方是主线程对象连接是跨线程队列投递没问题但如果你忘了moveToThread而对象其实是在主线程创建的那么connect一个工作线程发出的信号到主线程对象的槽函数也是队列投递同样没问题。真正容易出错的是“两个对象都以为自己在同一线程”的模糊状态——一定要明确每个QObject所属的线程这是线程安全的根基。我遇到过一种顽固的偶发崩溃排查到最后发现是有人在子线程的run()里new了一个QObject但没有moveToThread接管了它的生命周期后在主线程直接delete导致事件循环还在处理这个对象的事件时对象已被销毁。这类问题用Qt自带的方式很难发现所以我的经验是所有在子线程创建的对象要么绑定明确线程并统一销毁要么干脆用newdeleteLater的模式让事件循环来收尾。3.3 QMutex、QReadWriteLock与信号槽之外的原子操作多线程数据共享时QSignal没有帮你做任何数据同步信号只是触发通知具体数据还是需要通过共享内存、成员变量或者其它机制传递。Qt提供了QMutex和QMutexLocker做互斥锁如果读多写少可以用QReadWriteLock提升并发度如果是简单的整数标记或布尔值用QAtomicInteger让CPU帮助你完成原子操作省去加锁的开销。// 用QMutexLocker保护成员变量 void DataBuffer::append(const QByteArray data) { QMutexLocker locker(m_mutex); m_buffer.append(data); } QByteArray DataBuffer::takeAll() { QMutexLocker locker(m_mutex); QByteArray data m_buffer; m_buffer.clear(); return data; }这里有个值得养成的习惯无论锁粒度大小统一用QMutexLocker的RAII写法绝不用手动lock/unlock。因为一旦代码逻辑中途异常、提前return手动unlock很容易漏掉死锁就随之而来。RAII方式会在作用域结束时自动解锁这是C里最优雅的保护方式。4. 实操案例串口采集FFT频谱显示的完整多线程实现4.1 需求描述与线程模型选型为了把前面的知识串起来我用一个实际项目来演示。需求是这样通过串口接收传感器数据每包数据512个字节采样率10kHz要求实时显示时域波形并将最新一段数据做FFT转换显示频域波形界面还要支持暂停/继续、清空显示。这个需求如果全放主线程串口每收一包数据就触发一次槽函数槽函数里还要做FFT运算和重绘。FFT的运算量虽然不算夸张但QCustomPlot的replot()是完整重绘数据一密集必然卡顿。我选择的方案是串口对象放在主线程收到数据的信号直接连接到主线程的一个槽函数。槽函数里不处理、不绘制只把原始数据追加到DataBuffer共享缓冲区然后发出一个DataReady信号。主线程里再启动一个QThread里面放一个Worker对象专门负责从DataBuffer中取出数据进行FFT计算得到频谱数据后通过信号把结果传回主线程。主线程收到频谱结果后只做一件事更新QCustomPlot曲线。整个模型里数据流向是“串口→主线程缓冲区→工作线程FFT→主线程绘图”工作线程不碰任何UI主线程也不做耗时计算两边各司其职。4.2 关键代码实现与注释串口接收部分的示意代码// MainWindow构造函数中 m_buffer new DataBuffer(this); m_worker new Worker(m_buffer); m_workerThread new QThread(this); m_worker-moveToThread(m_workerThread); connect(serial, QSerialPort::readyRead, this, MainWindow::onSerialReadyRead); connect(this, MainWindow::startProcessing, m_worker, Worker::start); connect(m_worker, Worker::spectrumReady, this, MainWindow::onSpectrumReady); m_workerThread-start(); void MainWindow::onSerialReadyRead() { QByteArray data serial-readAll(); m_buffer-append(data); emit startProcessing(); // 触发worker处理 } void MainWindow::onSpectrumReady(const QVectordouble freqs, const QVectordouble amplitudes) { // 到这里一定在主线程可以直接操作UI ui-plot-graph(0)-setData(freqs, amplitudes); ui-plot-replot(); }Worker部分的实现Worker::Worker(DataBuffer *buffer) : m_buffer(buffer), m_running(false) {} void Worker::start() { if (m_running) return; m_running true; QByteArray data m_buffer-takeAll(); // 省略具体数据解析与FFT运算假设得到频率和幅度数组 QVectordouble freqs doFFT(data); QVectordouble amps computeAmplitudes(data); emit spectrumReady(freqs, amps); m_running false; }这里有三个要点第一startProcessing这个信号和Worker::start槽的连接因为Worker被moveToThread到了工作线程所以信号投递到工作线程的事件循环中执行。如果Worker被连续触发多次start槽函数也只会被一个个排队执行不会并发执行天然避免了race condition。第二串口数据是字节流不一定每包都恰好是512字节所以Worker里需要做粘包处理把缓冲区里的数据按帧切割不足一帧的先暂存这个逻辑在DataBuffer类内部完成。我在DataBuffer里维护了一个QByteArray m_cachetakeAll()的时候先把缓存拼上最新数据再切割多余的留在缓存里。第三m_running布尔值我这里用了普通成员变量严格意义上跨线程读写还不够安全但这里够用——因为start槽函数本身在工作线程串行执行主线程只负责发信号不会同时去写m_running。如果以后改成能并发调用的场景需要换成QAtomicInteger或者加锁。4.3 QCustomPlot时域转频域的联动与性能优化时域转频域这部分推荐用KISSFFT这个小巧的FFT库相比FFTW它体积小、无依赖、集成简单MIT协议商用友好。用qmake的话只需要把它添加进工程编译即可。要把时域曲线在QCustomPlot上同时显示常见的做法是分别创建两个graph一个显示时域数据一个显示频域数据或者把QCustomPlot放在两个不同的控件中。考虑到实时刷新性能我建议时域图的graph数据只保留最近1000个点频域图输出256个点的幅度谱。const int FFT_POINTS 256; kiss_fft_cfg cfg kiss_fft_alloc(FFT_POINTS, 0); // 填充时域采样数据到kiss_fft_cpx数组注意可能要加窗函数减少频谱泄漏 kiss_fft(cfg, fft_in, fft_out); // 计算幅度sqrt(re*re im*im)去除直流分量和对称部分后取前N/2个点操作频率上QCustomPlot的replot()如果每秒被调用太多次会非常吃CPU。我实际测试下来10kHz采样率、每秒产生20包数据的情况下每秒调用replot约20次是可以接受的但如果把数据包加大到512点、每秒50包就建议做一个简单的节流比如设定一个定时器每100ms重绘一次期间把多包数据累积到一个缓冲再一次性setData这样界面会流畅很多。4.4 线程退出与程序关闭的完整处理这个环节是很多项目的重灾区关闭程序时线程没有及时退出程序挂起几秒甚至直接crash。核心原则是先停业务再退线程最后删对象。MainWindow::~MainWindow() { // 1. 停止worker让run循环退出如果有while循环 m_worker-stop(); // 2. 退出事件循环 m_workerThread-quit(); // 3. 等待线程真正结束 m_workerThread-wait(); // 4. 线程结束后worker会被deleteLater自动回收 }如果你的Worker内部有while循环或者阻塞事件需要加一个volatile风格的停止标记在start()循环里定期检查。注意stop()这个槽是投递到工作线程执行的必须放在quit()之前否则事件还没执行事件循环就退了。还有一个细节connect(m_workerThread, QThread::finished, m_worker, QObject::deleteLater)这个连接要确保存在否则worker对象会内存泄漏或出现二次销毁问题。5. 常见问题与排查技巧实录5.1 高频问题速查表问题现象直接原因解决方案子线程更新UI程序偶发崩溃直接在非主线程操作了UI对象改信号槽队列投递或invokeMethod回主线程程序退出时卡死或crash线程没有正常退出或者对象销毁顺序不对严格按照“停业务→quit→wait→deleteLater”顺序moveToThread后槽函数不执行Worker没有事件循环或信号没触发确认thread-start()了确认connect的触发信号确实emit槽函数执行时不时的延迟很大事件循环被阻塞比如在槽里做了耗时操作把耗时操作拆到单独线程或者减少主线程阻塞多个工作线程同时修改同一份数据出错共享数据没有锁保护加QMutexLocker或改用QtConcurrent::mappedReduced避免共享信号槽连接了但没反应对象生命周期管理混乱对象已销毁或没正确moveToThread仔细检查connect使用上下文对象参数确保接收对象有效串口/网络数据收包粘包错乱字节流没有按帧解析用队列缓存按帧头帧尾切割不足一帧暂存5.2 定位多线程Bug的实用手段多线程bug是最难复现也最难定位的类型。我建议从这几个角度入手第一在槽函数开头和结尾加qDebug() QThread::currentThreadId()确认执行的线程符合预期。很多莫名其妙的bug查到最后是槽函数执行在错误的线程里。第二打开Qt的警告输出。在main()里加上qSetMessagePattern并把环境变量QT_FATAL_WARNINGS设为1可以让QObject的线程警告直接暴露。很多线程模型错误在运行时其实会有warning输出只是默认被淹没了。第三如果你的崩溃是偶发的试试编译成Debug版用AddressSanitizerASan跑一遍。Qt项目在CMake里加一行set(CMAKE_CXX_FLAGS -fsanitizeaddress -fno-omit-frame-pointer)就能启用它会帮你精准定位内存越界、double free这类问题。5.3 多线程面试高频考点与项目谈法这部分给正在准备面试的朋友参考。面试官问“你项目里怎么用多线程的”你不光要说用了QThread还要能说清楚为什么选它、线程之间怎么通信、怎么保证安全退出。我这边常被问到的点有信号槽的AutoConnection、QueuedConnection、DirectConnection区别以及实际项目中哪些场景被迫用过QueuedConnection。moveToThread和继承QThread的本质区别为什么Qt官方更推荐moveToThread。多个线程同时访问一个共享容器会出什么问题怎么解决答QMutex不够要能说出锁粒度、死锁、原子操作。你项目的线程模型画出来是什么样的谁在往工作线程投递任务结果谁接收。如果你用的是线程池请说明线程池大小怎么确定为什么不直接new很多线程。把上面的案例代码吃透这些问题都能展开得很好。重点不在于背答案而在于真的亲手调过、踩过坑能讲出细节。6. 打包发布时与多线程相关的隐藏坑6.1 windeployqt打出来的程序为什么换个机器就崩windeployqt是Qt官方提供的部署工具它会自动拷贝依赖的DLL和插件。但因为它只会根据可执行文件导入表扫描依赖如果可执行文件本身没有直接引用到Qt模块而你的业务代码用到了就可能导致DLL缺失。如果项目里用了多线程相关的模块如QtConcurrent需要在.pro里有QT concurrent否则相关DLL不会被部署进去。另外如果程序在开发机上正常运行但通过windeployqt打包后运行时提示“no qt platform plugin could be initialized”那说明Qt平台插件没被正确找到。这个问题解决方法是确保platforms文件夹里包含qwindows.dll且和主程序在同一级目录同时环境变量QT_QPA_PLATFORM_PLUGIN_PATH有指向platforms目录。打包时windeployqt通常会自动处理但如果你手动精简过目录就很容易踩坑。6.2 发布程序的多线程调试与日志策略发布版本遇到多线程crash时最有效的排查手段是日志。我强烈建议在程序里加一个日志系统用qInstallMessageHandler将qDebug的输出重定向到文件同时记录每条日志的时间戳和线程ID。这样用户反馈“程序崩了”的时候你拿日志一看就知道崩的时候哪些线程还在活跃、哪条日志之后没有后续往往能直接定位到问题点。日志系统也要注意线程安全多个工作线程同时输出日志时要用QMutex保护写文件操作否则日志可能交错、错乱甚至少行。这个点经常被忽略但对线上排查的帮助极大。提示正式发布时建议把qDebug输出级别单独控制避免日志文件被调试信息灌爆同时保留有效信息。我一般用QtFatalMsg、QtCriticalMsg、QtWarningMsg记录错误信息QtInfoMsg记录关键节点状态QtDebugMsg在发布版默认关闭。6.3 麒麟x86等国产平台的Qt多线程特殊适配相关热搜词里出现了qt离线安装 麒麟x86、麒麟系统安装qt说明不少人在做国产化适配工作。麒麟系统上的Qt多线程开发和Windows相比有几个值得注意的差异一是线程调度策略不同。麒麟的Linux内核默认CFS调度器一个忙碌的线程不会像Windows那样被快速抢占所以如果在工作线程里写了while(1)形式的高占用循环对界面响应的影响可能会更明显。解决方案是让工作线程适当地休眠比如每处理完一批数据QThread::msleep(1)或者在循环里等待条件变量。二是QThread::wait()的行为在Linux上受信号影响。个别情况下会出现wait()长时间不退出的现象多见于底层库阻塞了系统调用。解决办法是在等待前先调用requestInterruption()或设置退出标记并且避免在子线程里调用第三方阻塞库。三是打包问题。麒麟系统上Qt经常需要离线安装GCC和Qt库windeployqt在Linux平台上依赖ldd扫描依赖。如果目标机器和开发机的库路径不同很可能会出现“依赖库找不到”的运行时错误。解决方案是用linuxdeployqt工具配合自己构建的AppImage或复制到固定目录并设置LD_LIBRARY_PATH。这些点不算Qt多线程的核心内容但如果你真的部署到国产环境视野提前打开能省不少事。7. 多线程项目里的线程生命周期管理心得7.1 谁创建谁负责销毁的边界意识多线程程序写多了最大的体会是“线程生命周期”的设计比算法本身更重要。很多崩溃根源不是你代码写错了而是对象的创建线程、所在线程、销毁线程没有统一规划最终在某个时间点上出现了“交叉使用已销毁对象”或“跨线程delete”的情况。我的设计原则是每一个QObject子类都要明确回答三个问题——它在哪里被创建、moveToThread到哪个线程、最终谁负责销毁。把这三个问题的答案注释在类头部或者构造函数旁边后续维护代码的人一眼就能看懂。新添加的connect语句也要反复确认“这个信号是在哪个线程发射的、槽函数会在哪个线程执行”。另一个实用的技巧是尽量少用全局指针传递跨线程对象如果确实需要最好用线程安全的单例模式内部用QMutex保护初始化避免懒加载时出现双重初始化。7.2 贯穿项目的线程安全检查清单我自己在项目评审时通常会过一遍下面这个清单每次都能揪出问题明确标注每个QObject所属线程不依赖“应该”这种猜测。是否使用了跨线程直接调用函数是的话改成信号槽或invokeMethod。槽函数里是否有阻塞操作有的话是否可以拆分任务或放入线程池。是否所有共享数据都有锁保护读取方是否也加了锁只写不加锁也不行。事件循环什么时候退出线程是否等待结束超时时间设多少。析构函数是否按“停业务→退出事件循环→wait→deleteLater”的顺序执行。是否在非主线程里调用了UI方法用grep搜一遍代码确认没有。是否有new出来的QObject在主线程以外的位置被delete。这个清单由简入繁覆盖了从单一QThread到线程池再到跨线程对象传递的各类场景。每次写完多线程代码我都会按这个清单自查一遍这几年帮我挡掉了不少线上事故。多线程这个话题说起来是几个类、几个函数真正做好其实是“从设计到实现再到部署”的整套工程能力。希望这篇内容不是又多看了一篇教程而是你真的能带着里面的方法回去改进手上的项目——从把耗时操作从主线程里迁出来开始哪怕只优化了串口接收那一块界面流畅度的提升都会非常明显。本文还有配套的精品资源点击获取
返回列表