Qt篇: 多线程与UI组件的通信方式
GUI线程
GUI线程,也称为主线程,是Qt应用程序启动时默认创建的线程。它负责以下几个关键任务:
- 事件循环:GUI线程运行一个事件循环,处理用户输入、窗口事件、定时器事件以及其他信号和槽连接的事件;
- UI组件管理:所有与界面相关的组件(如窗口、UI组件)都必须在GUI线程中创建、修改和销毁。GUI线程确保这些组件能够安全地响应用户输入和更新显示内容。
为什么UI组件只能在GUI线程中创建
无论是Qt还是.Net,我们都需要养成习惯性编程,GUI是专门渲染UI的,子线程处理所有繁琐的业务处理,而不能在子线程中更新UI。
- 线程不安全的图形库:大多数图形库(包括Qt内部使用的图形系统)并非线程安全的。如果多个线程同时操作同一个UI组件,可能会导致竞争条件和不一致状态。为了避免这些问题,Qt要求所有UI组件的操作必须在单一线程(即GUI线程)中进行;
- 事件处理:Qt的事件处理机制依赖于主线程的事件循环来传递和处理事件。如果UI组件不在GUI线程中,无法保证事件被正确处理,可能会导致界面无法响应用户操作;
- 跨线程访问:在Qt中,跨线程直接访问对象的成员(尤其是UI组件)是危险的,可能引发数据竞争。Qt提供了一些机制(如QMetaObject::invokeMethod和QThread::moveToThread)来安全地跨线程调用对象的方法,但直接操作UI组件仍然是被禁止的。
通信方式
通信方式有很多种,定时器更新、资源共享等,但是综合几种方式,还是信号槽和自定义事件是最优解。
通信一:信号与槽
这个方法应该是大家在项目中常用到的方式了,它允许对象之间通过发射信号和接收槽来进行松耦合的通信。搭建方法简单,且保证了线程安全。
- 在子线程类中定义界面组件的更新信号(signalUpdate);
- 在界面类中定义更新界面组件的槽函数(onSetData);
- 使用异步方式连接的方式进行信号与槽之间的绑定。
// MyThread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QObject>
#include <QThread>
class MyThread : public QThread
{
Q_OBJECT
public:
explicit MyThread(QObject *parent = nullptr);
protected:
void run();
signals:
void signalUpdateUI(QString);
};
#endif // MYTHREAD_H
// MyThread.cpp
#include "mythread.h"
#include <QWidget>
MyThread::MyThread(QObject *parent) : QThread(parent){}
void MyThread::run(){
emit signalUpdateUI("MyThread run begin...");
for(int i=0; i<7; i++) {
emit signalUpdateUI(QString::number(i));
sleep(1);
}
emit signalUpdateUI("MyThread run end...");
}
// Widaget.cpp
// 绑定子线程的处理,并更新到QPlainTextEdit组件中
#include "widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent), m_pText(this)
{
m_pText.resize(200, 200);
m_pText.move(10, 10);
m_t.start();
QObject::connect(&m_t, MyThread::signalUpdateUI, this, Widget::onSetData);
}
Widget::~Widget(){}
void Widget::onSetData(QString data){
m_pText.appendPlainText(data);
}
通信二:自定义事件
通过自定义事件类来讲子线程处理的结果进行存储,然后在主线程通过事件处理event或eventFilter函数来处理事件存储的数据,整个过程虽然比信号槽方式繁琐,但是这种方式比较灵活,可以监控和拦截对象的事件。
- 创建自定义事件类来存储线程处理的数据;
- 使用postEvent()函数将该自定义事件类发送出去;
- 主界面重写事件处理函数event()或eventFilter(),实现对该事件类的处理或过滤。
使用自定义事件章节里的StringEvent类,在MyThread中通过postEvent方式发送事件,最后在Wigdet中进行事件处理,结果和使用信号槽方式一致:
// MyThread.cpp
#include "mythread.h"
#include <QWidget>
#include <QApplication>
#include "stringevent.h"
MyThread::MyThread(QObject *parent) : QThread(parent){}
void MyThread::run(){
QApplication::postEvent(parent(), new StringEvent("MyThread run begin..."));
for(int i=0; i<7; i++) {
QApplication::postEvent(parent(), new StringEvent(QString::number(i)));
sleep(1);
}
QApplication::postEvent(parent(), new StringEvent("MyThread run end..."));
}
// widget.cpp
#include "widget.h"
#include "stringevent.h"
Widget::Widget(QWidget *parent) : QWidget(parent), m_pText(this)
{
m_pText.resize(200, 200);
m_pText.move(10, 10);
m_t.setParent(this);
m_t.start();
}
Widget::~Widget(){}
bool Widget::event(QEvent *event) {
if(event->type() == StringEvent::type) {
StringEvent *strEvent = dynamic_cast<StringEvent*>(event);
m_pText.appendPlainText(strEvent->data());
}
return QWidget::event(event);
}