Qt应用运行时会自动创建一个UI线程(Qt为了防止多线程操作界面出现问题,有关界面的操作必须在UI线程中),这个线程也就是主线程。然而程序运行的时候经常会有复杂操作,若在主线中进行处理则UI界面会出现暂停卡死的现象。所以,为了良好的用户体验,我们通常将耗时较长的功能放在一个子线程中进行处理。
QObject::connect
在介绍Qt线程使用方法前,先说说QObject::connect函数,前四个参数用过信号槽的肯定都知道,主要说说第五个参数连接类型Qt::ConnectionType:
Qt::AutoConnection(自动连接):若信号在接收者所在的线程内发射,等同于直接连接;若信号不在接收者所在的线程发射,等同于队列连接。该种方式也是connect函数默认连接方式。
Qt::DirectConnection(直接连接):信号发射后直接调用槽函数,槽函数与信号所处同一线程。无论接收者所处哪个线程,槽函数都在发射信号线程执行。
Qt::QueuedConnection(队列连接):当控制权回到接收者所依附线程的事件循环时,槽函数被调用。若发送者和接收者所在不同线程,信号发送后,当接收者线程获得控制权后执行槽函数;若发送者与接收者在同一线程,该线程下次获得控制权时执行槽函数,类似于异步。
Qt::BlockingQueuedConnection(阻塞队列连接):槽函数的调用机制与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
Qt::UniqueConnection(唯一连接):该类型可以通过按位或与以上四个结合在一起使用。当该类型已设置,某个信号和槽已经连接时,再进行重复的连接就会失败。
Qt使用QThread来管理线程,一个QThread对象管理一个线程。QThread工作在子线程主要有两种方法:1.QObject对象调用movetothread方法;2.子类化QThread重写虚函数run。
moveToThread:
void QObject::moveToThread(QThread targetThread):一个对象所在线程通常是创建它的线程,该函数改变这个对象及其子对象所依附的线程为targetThread,如果该对象拥有父对象则函数调用失败,该方法默认开启事件循环。
使用该方法能够实现信号槽运行在子线程,具体代码及实现步骤如下:
- 子类化QObject,定义一个Work类。
- 实例化Work对象并调用moveToThread,将QThread作为参数进行传递。
- 定义onWork槽函数,该函数在子线程中执行,并进行connect连接线程开启信号。
- 开启线程,在子线程中执行槽函数。
Work::Work(QObject *parent)
: QObject(parent)
{
}
Work::~Work()
{
}
void Work::directCall()
{
qDebug() << "directThreadId" << QThread::currentThreadId();
}
void Work::onWork()
{
qDebug() << "workThreadId" << QThread::currentThreadId();
m_timer = new QTimer(this);
m_timer->setSingleShot(true);
connect(m_timer, &QTimer::timeout, [this](){
qDebug() << "timerThreadId" << QThread::currentThreadId();
});
m_timer->start();
}
Widget::Widget(QWidget *parent) :
QWidget(parent),
m_work(nullptr),
m_thread(nullptr),
ui(new Ui::Widget)
{
ui->setupUi(this);
//创建工作对象,不能指定父对象,否则moveToThread失败
m_work = new Work;
//创建子线程
m_thread = new QThread(this);
//将工作对象移动到子线程
m_work->moveToThread(m_thread);
//工作对象槽函数连接线程开启信号
connect(m_thread, &QThread::started, m_work, &Work::onWork);
}
Widget::~Widget()
{
delete ui;
//线程退出
m_thread->quit();
if (!m_thread->wait(100)) {
m_thread->terminate();
}
m_work->deleteLater();
}
void Widget::on_btnStart_clicked()
{
qDebug() << "mainThreadId" << QThread::currentThreadId();
if (m_thread->isRunning()) {
return;
}
//启动线程,发送started信号
m_thread->start();
//直接调用线程函数,线程函数所在线程与调用线程一致
m_work->directCall();
}
执行结果如下:
mainThreadId 0x3610
directThreadId 0x3610
workThreadId 0x966c
timerThreadId 0x966c
从执行结果可以看出直接调用函数所处线程与主线程一致,槽函数工作在子线程。顺手写了个子线程使用QTimer,使得timerOut槽函数工作在子线程。
重写run函数
void QThread::run():当线程调用start启动线程时会创建一个新的线程并调用run函数,run函数默认会调用exec开启事件循环,此函数返回会结束线程。
使用该方法能够使run函数工作在子线程,具体使用步骤如下:
- 子类化QThread,定义一个Thread类。
- 重写run函数,该函数运行在子线程。
- 实例化Thread,调用start开启线程。
下面子类化了一个QThread重写了run函数,添加了一个槽函数onSignalcall 和一个普通函数directCalll,并在其中打印线程id。
MyThread::MyThread(QObject *parent) :
QThread(parent)
{
//使用moveToThread(this),可以将槽函数运行在子线程,不要设置父对象
//moveToThread(this);
}
MyThread::~MyThread()
{
}
void MyThread::run()
{
qDebug() << "runThreadId" << currentThreadId();
//timer属于子线程 this属于主线程,若不加moveToThread(this),二者在不同线程。
//使用Qt::QueuedConnection相连,则定时器槽函数工作在接收者线程,即主线程。
//使用Qt::DirectConnection相连,则定时器槽函数工作在发送者线程,即子线程。
//若加上moveToThread(this),则this移动到子线程,无论如何相连都工作在子线程。
QTimer *timer = new QTimer();
timer->setInterval(1000);
connect(timer, &QTimer::timeout, this, [this](){
qDebug() << "timerThreadId" << currentThreadId();
}, Qt::DirectConnection);
timer->start();
//开启线程事件循环,若不调用exec则run函数返回线程终止
exec();
}
void MyThread::onSignalcall()
{
qDebug() << "signalcallId" << QThread::currentThreadId();
//onSignalcall接收主线程的信号
//若不加moveToThread(this),timer及this都依附在主线程,定时器槽函数工作在主线程。
//若加上moveToThread(this),则this依附在子线程,timer也依附在子线程,定时器槽函数工作在子线程。
//QTimer *timer = new QTimer(this);
//timer->setInterval(1000);
//connect(timer, &QTimer::timeout, this, [this](){
// qDebug()<< "timerThreadId" <<currentThreadId();
//});
//timer->start();
}
void MyThread::directCall()
{
qDebug() << "directThreadId" << QThread::currentThreadId();
}
Widget::Widget(QWidget *parent) :
QWidget(parent),
m_timer(nullptr),
m_thread(nullptr),
ui(new Ui::Widget)
{
ui->setupUi(this);
//创建子线程
m_thread = new MyThread(this);
connect(this, &Widget::callsignal, m_thread, &MyThread::onSignalcall);
}
Widget::~Widget()
{
delete ui;
//线程退出
m_thread->quit();
if (!m_thread->wait(100)) {
m_thread->terminate();
}
}
void Widget::on_pushButton_clicked()
{
qDebug()<< "mainThreadId" << QThread::currentThreadId();
if (m_thread->isRunning()) {
return;
}
m_thread->start();
m_thread->directCall();
emit callsignal();
}
执行结果如下:
mainThreadId 0x6e6c
directThreadId 0x6e6c
runThreadId 0xa014
signalcallId 0x6e6c
timerThreadId 0xa014
可以看出run函数运行在子线程,而槽函数和直接调用成员函数运行在主线程。这是因为QThread是用来管理线程的,它所在的线程和其管理的线程是两个线程。因为QThread是在主线程中创建的,所以其成员函数的调用和槽函数会在主线程中运行(使用movetothread可以使槽函数运行子线程中)。而且重写run后需要开启事件循环需要手动调用exec,否则执行上述代码后线程自动退出。