ARTICLE DETAIL

资讯详情

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

Qt数据交互核心:信号槽、多线程与MVVM架构实践

Qt数据交互核心:信号槽、多线程与MVVM架构实践 1. 数据交互的前置思考先搞清楚你要交互的是什么做 Qt 开发到现在我见过太多人在“数据交互”这件事上栽跟头。有人拿着QThread往界面线程里扔指针界面直接崩掉有人把数据库查询放到主线程界面卡成幻灯片还有人写了一大堆信号槽结果逻辑绕来绕去最后自己都不知道数据是从哪儿来的。先说结论Qt 里的数据交互本质上就三个问题——谁产生数据、谁消费数据、数据在哪个线程流动。搞清楚这三件事再谈具体技术才有意义。这篇文章我会从信号槽的底层机制讲起一路拆到线程间队列、MVVM 架构设计、高频数据节流最后附上我调试过程中踩过的坑和排查思路。无论你是刚接触 Qt 的初学者还是已经在用 QThread、movetoThread 做多线程开发的熟手这篇文章都值得你从头到尾读一遍。我为什么强调线程维度因为 90% 的“数据交互异常”都不是数据本身的问题而是线程归属搞错了。Qt 的QObject有明确的线程亲和性thread affinity一个对象归哪个线程管它的槽函数就会在哪个线程执行。很多人不理解这一点直接把worker对象扔给moveToThread然后主线程里访问 worker 的成员变量崩溃了还不知道原因。2. 信号槽是 Qt 数据交互的基石但你真的懂它吗2.1 Qt 元对象系统靠一串数字在传消息信号槽的本质是观察者模式的编译器级实现。Q_OBJECT宏会让编译器生成元对象代码每一个信号和槽都被编上索引号。当你在代码里写emit someSignal(data)的时候实际执行的是QMetaObject::activate()它拿着信号的索引去查找所有连接到这个信号上的槽函数索引然后逐个调用。这里有一个关键细节QObject::connect()建立连接的时候第五个参数ConnectionType决定了槽函数的执行方式。默认的Qt::AutoConnection会根据发送者线程和接收者线程的关系自动选择如果发送者和接收者在同一个线程走直连Qt::DirectConnection等同于直接函数调用同步执行。如果不在同一个线程走队列连接Qt::QueuedConnection把参数拷贝成一份QMetaCallEvent投递到接收者对象所在线程的事件循环里再由事件循环取出执行。这个自动选择机制是 Qt 跨线程安全传递参数的根本保障。你甚至可以手动指定Qt::BlockingQueuedConnection让发送线程阻塞等待接收线程执行完槽函数——这在某些需要同步结果的场景下很有用但如果使用不当很容易造成线程互相等待、直接死锁。我的原则是能不用就不用除非你非常清楚接收线程正在正常处理事件。2.2 为什么说队列连接就是 Qt 内置的线程安全数据管道我再强调一次这个概念队列连接本身就是一个线程安全的队列。信号发射时Qt 会把参数复制到堆上以事件的形式压入接收者线程的事件队列。这个投递过程由 Qt 内部互斥锁保护你不需要额外加锁。接收者线程在exec()事件循环中不断取事件、分发处理自然就把数据从生产线程挪到了消费线程。这给了我们一个非常重要的启示如果你要在一个线程里往另一个线程发数据优先考虑用信号槽而不是自己建一个QQueue加锁去保护。前者是 Qt 官方打磨了二十多年的成熟通道后者很容易在wait()和notify()之间出竞态排查起来相当痛苦。我项目里就曾经为了省一次信号调用自己写了个线程安全队列结果上线后偶现数据丢失查了整整两天最后换回信号槽问题瞬间消失。注意在跨线程使用队列连接时信号参数的类型必须是通过qRegisterMetaType()注册过的或者自带元类型支持的类型否则connect的时候会直接给你一个编译错误类似QObject::connect: Cannot queue arguments of type MyData。这是新手最常见的报错之一下面我们会专门讲。3. 线程之间的数据交互生产者消费者模型落地3.1 为什么说生产者消费者是项目里最稳的数据交互架构在实际项目中尤其是数据采集、界面控制这类场景生产者消费者模型几乎是必答题。采集线程不断往系统里塞数据界面线程需要按一定节奏刷新如果采集线程直接去操作界面控件轻则界面闪烁重则崩溃。更合理的做法是把采集线程当成生产者把界面线程当成消费者中间用一条“数据通道”分开。热词里频繁出现“qt多线程 生产者 消费者”说明这是大家绕不开的需求。我给出一个我至今仍然在用的标准实现方案用一个自定义工作线程类在run()里执行生产者逻辑比如循环读取采集卡数据每取到一包数据就emit dataReady(package)主线程连接这个信号到界面刷新槽函数上。因为两个线程不在同一个线程里Qt 自动选择队列连接数据包就安全地“漂”到了主线程。class ProducerThread : public QThread { Q_OBJECT public: ProducerThread(QObject* parent nullptr) : QThread(parent), m_running(false) {} ~ProducerThread() { stop(); } void stop() { m_running.store(false); quit(); // 退出事件循环 wait(); // 等待线程真正结束 } protected: void run() override { m_running.store(true); int counter 0; while (m_running.load()) { QByteArray rawData readFromHardware(); // 模拟采集 emit dataReady(rawData, counter); QThread::msleep(50); // 模拟采样周期 } } signals: void dataReady(const QByteArray data, int seq); private: std::atomicbool m_running; };主线程这边你只需要一个普通的槽函数class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget* parent nullptr) : QMainWindow(parent) { auto* producer new ProducerThread(this); connect(producer, ProducerThread::dataReady, this, MainWindow::onDataReady); producer-start(); } private slots: void onDataReady(const QByteArray data, int seq) { // 这里一定运行在主线程直接更新控件即可 m_label-setText(QString(第 %1 包数据大小 %2 字节).arg(seq).arg(data.size())); } };你可能会问为什么dataReady信号发射时没有加锁因为connect已经建立了队列连接QByteArray传入信号槽时会通过隐式共享机制拷贝一份数据指针而不是把指针本身传过去。数据本身不会跨线程共享所以安全。3.2 moveToThread 才是真正的现代写法很多教材还在教你子类化QThread去重写run()但在 Qt 官方最新的推荐里更受欢迎的方式是使用QObjectmoveToThread()。思路是创建一个普通QObject子类worker把它的线程亲和性移动到子线程然后所有通过信号槽触发的方法都会在子线程里执行。class Worker : public QObject { Q_OBJECT public slots: void doWork(const QString param) { for (int i 0; i 10; i) { QThread::msleep(100); emit progress(i 1); } emit finished(); } signals: void progress(int percent); void finished(); };启动方式auto* worker new Worker(); auto* thread new QThread(); worker-moveToThread(thread); connect(thread, QThread::finished, worker, QObject::deleteLater); connect(this, MainWindow::startTask, worker, Worker::doWork); connect(worker, Worker::finished, thread, QThread::quit); thread-start(); emit startTask(task-001);这段代码的精髓在于所有对象的方法调用都通过信号槽间接完成你永远不会直接跨线程调用worker的方法。如果你真的在子线程里直接调用了doWork()那和普通函数调用没有区别moveToThread就白写了。这也是很多面试官爱问的一点moveToThread改变的是对象的线程亲和性而不是让你跨线程随意访问它。热词里有“qt, movetothread”和“qt::movetothread”的频繁搜索说明这个 API 确实是大家的痛点。我再补充一个细节moveToThread必须在对象没有父对象时调用否则会失败并输出警告。还有一个坑是当 worker 对象有定时器时定时器归属也随之迁移到新线程这样定时器的超时槽就会在新线程执行非常有用。3.3 队列连接和事件循环的依赖关系我在这里必须着重强调一个极易被忽略的暗坑只有接收者线程的事件循环在运行队列连接才能生效。如果你在子线程里用Worker处理任务但这个线程没有运行事件循环比如直接调用子线程中的QThread::run()做耗时计算那么所有投递给该线程的队列事件都会堆积永远得不到处理。这就是为什么moveToThread之后必须在主线程里调用thread-start()而线程启动后还要保证exec()被调用——QThread::run()的默认实现就是调用exec()这就是事件循环。如果你的子线程完全是自己写的字节流处理不接受任何信号槽投递的队列那关闭事件循环也不会出问题但一旦涉及跨线程信号就必须保证事件循环存在。提示排查队列连接失效时先检查接收者线程的事件循环状态。最原始的办法是给线程加日志到达exec()前打印一条“event loop started”如果没有这条日志说明线程根本没跑起来后续信号处理自然无从谈起。4. 数据类型的选择值语义、隐式共享和元类型注册4.1 为什么 QVariant 和自定义结构体这么容易出问题从热词里能看到“qt double转字符串”、“qt结构体”、“qt,写入内存缓冲区,结构体”的需求很旺盛。这些需求都指向同一个根源** Qt 信号槽传递数据时对类型有很高的要求**。如果你在信号里传了一个自定义结构体但没有做元类型注册那么在某些连接模式下跨线程队列连接、QML 调用程序会直接报错或崩溃。正确的做法分两步struct SensorData { double temperature; double humidity; quint32 timestamp; }; Q_DECLARE_METATYPE(SensorData)然后在程序启动时比如main()函数里qRegisterMetaTypeSensorData(SensorData);Q_DECLARE_METATYPE让这个结构体能够被QVariant监禁qRegisterMetaType则让它在队列连接时可以被完整拷贝投递。很多人只做了第一步就跨线程发信号结果链接是建立了但参数在拷贝时被丢弃或者干脆发不过去——这属于典型的“编译期没问题运行期一片模糊”的故障。4.2 隐式共享让你可以放心“传值”但别被指针晃了眼Qt 的核心容器QString、QByteArray、QList、QImage等都是隐式共享的。什么叫隐式共享就是拷贝一个容器时只是复制了指向内部数据的指针和引用计数真正的数据只有一份只有当你对其中一个副本进行“写”操作时系统才会立刻复制一份数据保证两个副本各自独立。这极大地降低了值传递的成本。所以你在信号槽里传QByteArray或者QListQPointF放心大胆地传值性能上是没问题的。但如果某天性能分析发现这里成了瓶颈再考虑改用const QByteArray也不行——跨线程队列连接里Qt 要拷贝数据进事件对象你传引用传值的效果其实一样只是多一次编译器优化。真正需要小心的场景是你用裸指针传数据。如果生产线程 new 了一个对象把指针塞进信号槽消费线程去读这个指针那么生产线程必须保证这个对象在整个消费期间不销毁。实际操作中这种裸指针方案十有八九会在内存管理上抽风。我的建议是能不传指针就不传指针真的非要传请用QSharedPointer并记住队列连接不会自动拷贝QSharedPointer指向的数据只是把智能指针复制一份数据生命周期由智能指针维护安全得多。4.3 double 转字符串这些小事为什么也值得一提“qt double转字符串”这个热词让我意识到很多开发者的数据交互卡在了最基础的格式化环节。Qt 里 double 转字符串有几个容易踩的坑我列在这里QString::number(3.1415926535, f, 2)保留两位小数结果是3.14。QString::number(3.1415926535, g, 6)有效数字 6 位结果是3.14159。QString::number(3.1415926535)默认有效数字 6 位结果可能让你惊讶——不是3.14159而是3.14159默认精度就是 6。很多人在做实时数据显示时直接把 double 塞进QString(温度: %1℃).arg(value)显示出来可能变成一堆科学计数法因为默认格式化可能触发g标准。正确的做法是先QString::number(value, f, 2)再拼接或者用QString::asprintf(%.2f, value)。这些看似基础的小细节在数据交互里其实就是“最后一公里”。数据从底层采集上来再漂亮格式化错了界面看到的也是错的值。5. QML 的数据交互MVVM 架构才是终极答案5.1 为什么 QML 里不能像 Widgets 一样随便操作控件Qt Widgets 时代大家习惯在代码里直接label-setText()。可到了 QML一切都变了——QML 的数据绑定的核心是属性绑定界面上你看到的每一个文字、每个条目的颜色都可能是某个 C 对象的某个属性经过绑定得到的结果。如果你在 C 端直接改界面的属性比如findChildQQuickItem*后再去设置setProperty那基本等于绕过了 QML 引擎的响应机制页面不会自动刷新。所以 QML 项目里的数据交互最佳实践是MVVM 架构Model业务数据层比如传感器数据、用户信息通常是 C 类或数据库。ViewModel专门为 View 准备的数据模型把 Model 的数据“翻译”成界面可读的格式暴露成 Q_PROPERTY 或 Q_INVOKABLE。ViewQML 界面只负责显示 ViewModel 暴露的属性和调用命令方法。这样做的好处是界面层完全不关心业务逻辑数据源换了、单位换了界面不用大改。热词里出现了“qt mvvm框架”说明这个方向在 Qt 社区逐渐升温我甚至见过有人把 Qt 的 MVVM 做成通用模板支持属性通知和命令绑定类似 WPF 的绑定机制。5.2 用 Q_PROPERTY 和信号实现属性通知要让 QML 里的绑定自动刷新你的 ViewModel 必须继承QObject并使用Q_PROPERTY声明可绑定属性。每一次属性值变化都要发出对应的notify信号。比如class SensorViewModel : public QObject { Q_OBJECT Q_PROPERTY(double temperature READ temperature NOTIFY temperatureChanged) public: explicit SensorViewModel(QObject* parent nullptr) : QObject(parent), m_temperature(0.0) {} double temperature() const { return m_temperature; } public slots: void onSensorData(const SensorData data) { if (qAbs(m_temperature - data.temperature) 0.01) { m_temperature data.temperature; emit temperatureChanged(); } } signals: void temperatureChanged(); private: double m_temperature; };在 QML 端Text { text: vm.temperature.toFixed(2) °C }只要vm是暴露给 QML 的SensorViewModel实例那么 C 端一旦更新属性并发出信号QML 的文本就会自动刷新。这种“数据交互”看起来是信号槽在工作实际上背后是 QML 引擎监听 notify 信号并重新解析绑定表达式。这里我想特别提醒一个细节只在值真的变化时才发送 notify 信号。像我在上面的代码里用qAbs做了一个阈值判断如果数据没变还不停发信号QML 会反复重算绑定高频数据下界面表现会非常差。这一点在下面讨论高频数据节流时还会再提到。5.3 命令绑定让 QML 调用 C 方法并且能控制可点击状态数据是 C 到 QML 的单向流用户的点击、输入则是反向的命令流。MVVM 里一般用Q_INVOKABLE方法或QObject的 public slot 暴露命令。QML 端可以直接调用Button { text: 启动采集 onClicked: vm.startCollection() enabled: vm.isReady }startCollection()是Q_INVOKABLE方法isReady是一个Q_PROPERTY。当采集未就绪时按钮自然就是灰色的。这个模式的好处在于界面的可交互性也是由 ViewModel 控制的C 层可以随时改变按钮的可用状态不需要 QML 端写一堆条件判断。提示如果想做更彻底的 MVVM 命令系统可以实现一个通用的Command类内部持有std::function暴露execute()方法和canExecute属性。这样 QML 端拿到一个命令对象按钮点击后调用命令命令自己判断能否执行、是否正在执行。我用这种方式封装过十几个项目逻辑清晰测试也方便。6. 高频数据流如何优雅地节流而不丢数据6.1 事件队列会被撑爆吗很多场景下数据交互的难点不在“能不能交互”而在“交互多快”。比如传感器每秒产生几千个数据点界面根本来不及刷新那么快如果每个数据都通过队列连接投递到主线程主线程的事件队列就会不断积压内存越涨越高界面越来越卡。我见过有人把这种问题归咎于 Qt 性能差其实不是。Qt 的队列连接每秒可以传输数万条事件瓶颈在于消费端无法跟上生产速率。解决办法不是降低生产速率可能数据必须采集而是在消费端做采样或合并。最常见的方案是节流刷新生产者仍然每个周期发射信号但消费端的槽函数不直接更新界面而是只写入一个“最新值”缓存同时启动一个定时器定时器每隔 100ms 检查缓存把最新的值刷到界面上。这样即使生产速率是一秒钟 1000 个信号界面也最多刷新 10 次每次显示的都是“最新值”。void MainWindow::onDataReady(const QByteArray raw, int seq) { // 这里依然运行在主线程但只更新缓存不做重活 m_latestRaw raw; m_latestSeq seq; } void MainWindow::onRefreshTimer() { // 定时器每 100ms 触发一次真正执行 UI 更新 m_label-setText(QString(最新包: %1 (%2 bytes)) .arg(m_latestSeq).arg(m_latestRaw.size())); }注意这里onDataReady仍然会被高频调用但它做的事很少所以不会阻塞主线程太久真正的 UI 更新交给定时器节奏可控。6.2 用脏标记合并重复刷新如果你不想用定时器另一个办法是脏标记数据到达时如果界面已经“脏”了说明有刷新任务在排队就不再投递新的刷新请求否则标记为脏投递一次刷新请求。本质上是把多次刷新合成一次。下面是核心逻辑void MainWindow::onDataArrived() { if (m_pendingRefresh) return; m_pendingRefresh true; QMetaObject::invokeMethod(this, doRefreshUI, Qt::QueuedConnection); } void MainWindow::doRefreshUI() { m_pendingRefresh false; // 真正刷新 }m_pendingRefresh是一个原子布尔量因为onDataArrived可能被多个信号调用但不必加锁——只要它运行在同一个线程中普通bool就够了。这里的关键是invokeMethod以队列方式投递了一个doRefreshUI调用后到的数据不会继续投递直到刷新完成。我个人在实际项目中更偏爱脏标记方案因为它不需要额外定时器响应也更及时但如果刷新本身需要固定节奏比如动画定时器方案更合适。两种方案可以组合视需求而定。6.3 QML 的数据流也要节流QML 端的属性绑定同样会受到高频数据的影响。假设你的 ViewModel 里有一个frequency属性采集线程每秒更新几千次那么 QML 界面会疯狂地重绘文本。前面提到我建议在 C 层用阈值判断是否真正发信号。QML 端还有一层保险用Timer控制显示更新频率让绑定值只在一秒内更新固定次数。我在项目里通常会在 ViewModel 里专门实现一个“批量提交”机制槽函数收到数据后先写入一个临时变量然后通过一个 30ms 的QTimer定期统一提交给 Q_PROPERTY。这样 QML 的刷新频率被人为限制在每秒 33 帧以内界面平滑CPU 占用也低。算是把“节流”下沉到了数据模型层。7. 跨线程调用的正确姿势invokeMethod、信号槽与线程池对比7.1 三种跨线程调用的优劣除了信号槽Qt 还提供了QMetaObject::invokeMethod、QtConcurrent::run、QThreadPool等工具它们各有适用场景。我根据自己的项目经验整理了一张对比表方式是否支持参数传递是否自动排队是否返回结果适用场景信号槽队列连接支持任意注册类型是入事件队列无事件驱动、异步通知QMetaObject::invokeMethod支持是指定连接类型支持返回值主线程更新界面、定向调用QtConcurrent::run支持参数线程池排队通过 QFuture 返回多任务并行计算QThreadPool QRunnable参数需自己封装线程池排队自己实现重复性任务池化热词里频繁出现“qt命令行工具”、“qt多线程”说明很多人在做控制台或后台任务类的项目。我建议能用信号槽解决的不要用QtConcurrent::run手动管理线程能用QtConcurrent::run解决的不要用裸QThread。线程资源本身是稀缺资源滥用会带来频繁的上下文切换性能反而下降。7.2 主线程更新界面的铁律所有 UI 更新都必须发生在主线程。这个结论即使在 QML 时代同样成立——QML 引擎的所有渲染和脚本执行都在主线程。所以当你从工作线程返回数据并需要刷新界面时要以Qt::QueuedConnection投递一个信号或invokeMethod到主线程。很多新手会问为什么我明明只在子线程里改了控件的setText也没崩溃那是因为 Qt 有部分操作内部会加锁或延迟刷新让你侥幸没炸。但这种侥幸不可靠线程调度一旦变化就可能触发数据竞争。我强烈建议养成铁一般的习惯任何 UI 操作都通过信号槽转到主线程哪怕你觉得“应该没事”。7.3 线程池与协程的补充如果你有大量同质化任务比如处理几千个图片缩略图用QtConcurrent::mapped或QThreadPool比手动QThread高效得多。QThreadPool会根据 CPU 核心数自动调度线程避免手动创建几百个线程导致的资源耗尽。另外Qt 6 已经原生支持协程qlcoroutine.h在写异步数据流时可以写出“串行代码”的效果非常推荐高级读者学习。不过协程目前依然是进阶内容我不建议新手立刻上手。先把信号槽和moveToThread吃透再考虑协程优化不迟。数据交互的核心永远是“正确性优先”别为了花哨的写法牺牲可维护性。8. 数据交互的十个典型事故现场与排查思路8.1 崩溃、信号不触发、QML 刷新失灵在 Qt 社区里最常被问到的几个问题我用表格列一下都是我实际踩过或帮别人定位过的现象可能原因排查思路程序启动直接崩溃跨线程直接调用了对象方法检查是否绕过了信号槽直接操作对象检查moveToThread后是否仍直连调用信号没触发类型未注册 / 事件循环没跑用qRegisterMetaType注册类型确认接收者线程执行了exec()QML 属性不刷新notify 信号没发或属性名错误检查Q_PROPERTY的NOTIFY信号名是否与代码一致打印信号触发日志UI 卡死高频数据刷爆事件队列使用脏标记合并刷新或限制刷新频率connect编译失败参数类型不兼容检查是否缺少Q_DECLARE_METATYPE或参数是自定义类型但没有qRegisterMetaType子线程使用父对象对象不能移动线程确保没有父对象后再调用moveToThread使用wait()导致卡死线程事件循环未退出等不到线程结束检查quit()是否被调用确保所有队列事件已经被处理队列连接中参数为空指针传递后原对象已销毁改用值语义或QSharedPointer8.2 崩溃定位的实用技巧给信号槽加日志排查数据交互问题最有效的第一步在信号发射处和槽函数入口各打印一行日志带上线程 ID 和对象指针。Qt 提供了QThread::currentThreadId()可以拿到当前线程 ID。如果发射端和接收端线程 ID 不一致说明队列连接生效如果一致却不触发多半是连接类型或事件循环的问题。把日志加完之后再看信号触发的频率。如果槽函数根本没被调用直接定位是连接建立还是类型注册的问题如果被调用了但界面没更新问题就缩小到了 GUI 刷新那一层。层层缩小范围比盲目翻代码高效得多。8.3 永远不要忽略 QML 引擎的错误提示使用 QML 时控制台里经常打印一长串英文错误比如TypeError: Cannot read property temperature of null。很多新手会忽略这些提示只盯着 C 代码。实际上QML 端的错误通常是数据交互链断裂的最直接信号——vm对象没暴露、属性名拼错、数据模型初始化失败都会有提示。先解决 QML 控制台的报错再回头看 C 逻辑往往事半功倍。我还经常建议项目里开一个全局的QtMessageHandler把所有 qWarning 和 qCritical 统一归档到文件。上线后的数据交互问题大多数都能从日志文件里直接找到线索而不是靠用户截图。9. 数据交互架构设计的最终建议从代码规范到集成要点说了这么多技术细节最后想聊一聊架构层面的事。数据交互从来不是“写几个信号槽”那么简单它贯穿了 Qt 项目的每一层数据源层采集线程、数据库、网络请求保证稳定输出数据。数据通道层信号槽、队列、线程池保证数据安全流动。数据模型层结构体的定义、元类型注册、序列化保证数据格式一致。界面层Widgets 或 QML只消费模型层暴露的数据不直接触碰业务逻辑。我见过太多 Qt 项目最后变得不可维护不是因为某个信号写错了而是因为数据在每一层都有不同的形状C 端维护一份QML 端又复制一份最后两边的数据对不上。所以无论是做桌面工具还是嵌入式界面我都建议用 MVVM 思路统一数据流底层统一用结构体或 DTOData Transfer Object承载业务数据。中间层用 ViewModel 把 DTO 转化成界面消费的显示模型。所有跨线程传输都走信号槽或队列不裸传指针。所有 UI 刷新都跑到主线程用节流合并高频更新。这样即使将来换了采集硬件、改了界面框架数据交互的核心链路也不用大改。另外关于热词里提到的“qt mel”、“qt vue3”、“cypress qt 控制”这些词我觉得有必要提一句Qt 和 Web 技术栈的联动本质上也是数据交互的扩展场景。通过QWebChannel或本地 WebSocket 桥接你完全可以在 QML 里嵌一个 Vue3 页面或者让 Cypress 通过 WebSocket 控制 Qt 应用。这些思路在架构上和前面讲的内容一脉相承只是传输介质从进程内信号槽变成了进程间通信。如果将来有机会我会专门写一篇 Qt 与 Web 前端的混合架构文章这里先立个 flag。写到最后我真心建议你把“数据交互”四字拆开来看——“数据”是内容“交互”是机制“线程”是边界。只要每次写信号槽之前先问自己三个问题谁生产谁消费在哪个线程80% 的坑都能提前绕开。剩下的 20%用日志和耐心去排查总能解决。这也是我这几年来做 Qt 项目最值钱的经验。
返回列表