Qt多线程之QThread

2023-11-19

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,如果该对象拥有父对象则函数调用失败,该方法默认开启事件循环。
使用该方法能够实现信号槽运行在子线程,具体代码及实现步骤如下:

  1. 子类化QObject,定义一个Work类。
  2. 实例化Work对象并调用moveToThread,将QThread作为参数进行传递。
  3. 定义onWork槽函数,该函数在子线程中执行,并进行connect连接线程开启信号。
  4. 开启线程,在子线程中执行槽函数。
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函数工作在子线程,具体使用步骤如下:

  1. 子类化QThread,定义一个Thread类。
  2. 重写run函数,该函数运行在子线程。
  3. 实例化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,否则执行上述代码后线程自动退出。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Qt多线程之QThread 的相关文章

随机推荐