Qt中的多线程

本文详细介绍了Qt中多线程的管理,包括线程的基本概念、创建、启动、退出,以及线程同步的QMutex、QReadWriteLock、QSemaphore和QWaitCondition的使用方法。强调了线程安全的重要性,提供了具体的示例代码,帮助理解如何在Qt应用程序中正确使用多线程。

1 线程的基本概念

通俗地来说,线程是进程中实际执行代码的最小单元,它由操作系统安排调度(何时启动、何时运行和暂停以及何时消亡)。在一个进程中,线程是实际干活的单位。因此一个进程至少得有一个线程,我们把这个线程称之为“主线程”。

在Qt中,如果管理线程的线程对象被销毁时该线程仍在运行,则程序将会报告异常。所以在Qt程序中,如果退出主线程时仍有子线程在运行,程序将会报告异常。除非管理这些子线程的对象在程序退出时不会被销毁。例如:

// 正常退出的程序
int main(int argc, char *argv[])
{
    SubThread *subThread = new SubThread;
    subThread->start();
    return 0;
}

// 退出异常的程序
int main(int argc, char *argv[])
{
    SubThread subThread;
    subThread.start();
    return 0;
}

退出异常提示如下图所示:
QThread退出异常提示
第一段程序虽然正常退出了,但是发生了内存泄漏。第二段程序则是直接在退出时发生了异常。所以在使用多线程时,一定要确保线程的正常退出。

2 线程的创建、启动和退出

在Qt中,线程的创建和使用是依赖于QThread这个类。QThread提供了一种依赖于操作系统的线程管理方式。在程序中,每个QThread对象只管理一个线程,而每个线程都是从QThread类提供的run()函数开始执行。

2.1 QThread类创建线程的两种做法

2.1.1 继承QThread。

通过继承QThread类来重写QThread的run()函数。从而在run()函数中实现我们需要完成的功能。(QThread自身的run()函数是protected且virtual的)。如:

class SubThread : public QThread
{
    Q_OBJECT
public:
    explicit SubThread(QObject *parent = nullptr);

signals:

    // QThread interface
protected:
    void run() Q_DECL_OVERRIDE;
};

void SubThread::run()
{
    qDebug() << "sub thread start";
    qDebug() << "sub thread id=" << QThread::currentThreadId();

    int i = 0;
    while (i <= 1000)
    {
        if (i % 100 == 0)
        {
            qDebug() << i << " ***  sub thread is running....";
        }
        i++;
    }
    qDebug() << "sub thread exit";
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug() << "main thread id = " << QThread::currentThreadId();
    SubThread sub;
    sub.start();
    return a.exec();
}

上述程序的运行结果为:
在这里插入图片描述

2.1.2 使用moveToThread()方法将一个QObject对象移动到一个QThread对象中

这种方法用到两个对象。一个是实现功能的worker对象,一个是提供线程能力的QThread对象。步骤如下:

  • 第一步:创建worker。如:

    // 实现功能的worker
    class ThreadWorker : public QObject
    {
        Q_OBJECT
    public:
        explicit ThreadWorker(QObject *parent = nullptr);
    
        void doWork(); // 作为线程处理函数
        void exit();
    
    private:
        bool m_exit;
    
    };
    
    void ThreadWorker::doWork()
    {
        qDebug() << "worker start";
        qDebug() << "sub thread id=" << QThread::currentThreadId();
        m_exit = false;
        int i = 0;
        while (!m_exit && i <= 1000)
        {
            if (i % 100 == 0)
            {
                qDebug() << i << " ***  worker is running....";
            }
            i++;
        }
        qDebug() << "worker exit";
    }
    
    void ThreadWorker::exit()
    {
        m_exit = true;
    }
    
  • 第二步:将worker移入另外的线程管理对象中。

        QThread *subThread = new QThread(this);
        m_worker = new ThreadWorker;
        m_worker->moveToThread(subThread);
    
  • 第三步:启动线程。这里启动线程,不仅要调用QThread的start()方法,还需要调用worker的线程处理函数。上例中即是doWork()函数。对于start()调用,则是在调用线程中直接执行subThread.start()即可,而对于doWork()的调用,则是必须通过信号与槽方式来执行。这是为了确保对doWork()的调用是在不同的线程中。这里分为两种情形:
    (1). 先调用了start(),然后根据用户操作和程序执行情况触发子线程处理信号,然后在该信号对应的槽中调用doWork()。如:

        Widget::Widget(QWidget *parent)
    	    : QWidget(parent)
    	{
    	    qDebug() << QThread::currentThreadId();
    	    QThread *subThread = new QThread(this);
    	    m_worker = new ThreadWorker;
    	    m_worker->moveToThread(subThread);
    	    QPushButton *startBtn = new QPushButton(this);
    	    startBtn->setText("start");
    	    startBtn->setGeometry(0,0, 100, 50);
    	    connect(startBtn, &QPushButton::clicked, this, &Widget::startWorker);
    	    connect(this, &Widget::workerStart, m_worker, &ThreadWorker::doWork);
    	    subThread->start();
    	}
    	
    	void Widget::startWorker()
    	{
    	    emit workerStart();
    	}
    

    这里,doWork()是作为workerStart信号的槽被调用的。this是当前线程的对象,而m_worker是subThread中的对象。这两个对象处于不同的线程中,所以这个connect的连接方式是Qt::QueuedConnection。而对于Qt::QueuedConnection连接方式,槽函数是执行在接收信号的对象所在的线程中的。所以,doWork()是执行在其他线程中。
    (2). 在用户操作和程序执行情况触发子线程处理信号时,调用start(),然后在QThread的started信号的槽函数中调用doWork()。如:

    Widget::Widget(QWidget *parent)
        : QWidget(parent)
    {
        qDebug() << QThread::currentThreadId();
        m_thread = new QThread(this);
        m_worker = new ThreadWorker;
        m_worker->moveToThread(m_thread);
        QPushButton *startBtn = new QPushButton(this);
        startBtn->setText("start");
        startBtn->setGeometry(0,0, 100, 50);
        connect(startBtn, &QPushButton::clicked, this, &Widget::startWorker);
        QObject::connect(m_thread, &QThread::started, [=](){
              qDebug() << QThread::currentThreadId();
            m_worker->doWork();
        });
    }
    void Widget::startWorker()
    {
        m_thread->start();
    }
    

    这里是通过匿名槽函数来调用doWork()的。而对于槽函数是匿名函数的connect,其连接方式为Qt::DirectConnection,其槽函数执行在发出信号的线程中。所以,这里的doWork()也是执行在其他线程中。

    针对上面两种情形,(1)中在线程处理函数被调用之前就启动了线程,这可能会导致资源上的一些浪费;而(2)中在每次操作时去启动线程,则在每次启动时必须先检查线程的状态,这无疑会增加逻辑控制的复杂度。

在上例中moveToThread()的参数除了是QThread对象外,也可以是QThread的子类对象。如上面的SubThread的对象。如果使用的是QThread的子类对象,则需要注意的是QThread的run()方法中默认包含一个event loop。所以,使用的子类对象的run()中也需要提供一个event loop。否则线程会提前退出从而导致doWork()函数无法执行。在上面所述的(1)情形中,由于先调用的start(),所以run()会优先执行,如果没有event loop,则在run()执行完成后线程就将退出了,所以在触发doWork时,其将不会执行。而在(2)情形中,doWork()会优先于run()执行,所以对于(2)中的调用,即使run()中没有event loop,doWork()也会执行一次。但是如果doWork中没有阻塞处理,doWork就将只能运行一次,因为doWork已经返回,这时再启动线程也就毫无意义了。如果doWork中有阻塞,则线程的start()函数可能都无法北调用,因为程序发生了阻塞,所以这也毫无意义。因此,线程的start()一定要早于doWork被调用。

2.2 线程退出

正如前面所述,如果线程未正常退出,则在销毁线程管理对象时将会发生异常。而要使线程退出,一种方法是使QThread的run()函数正常返回;另一种方法是强制终止线程。QThread类提供了terminate()函数来强制终止一个线程。不过该方法的最终执行依赖于系统的时间调度,所以其行为是不可控的。而且使用此函数时,线程不能清理其之前创建的一些资源,所以并不推荐使用这种方法。通常是使用QThread提供的quit()和wait()函数来使一个线程退出。

  • quit():quit是使run()函数中的event loop退出。比如,run()中含有一个名为threadLoop的event loop,则quit的作用相当于threadLoop.exit()。所以,如果run()函数中没有event loop,则quit()函数并不起什么作用;而如果run()函数含有其他循环处理,在要是run()函数返回,则必须先要退出这个循环处理。如上面SubThread中的run()函数,其中并没有event loop,所以要使其退出,只能等待其执行结束。如果循环处理时间比较长,则可以通过设置标识来控制循环退出。上面ThreadWorker中的m_exit,这就是一个控制循环退出的标识。通过ThreadWorker使用线程的退出示例如下:
    void Widget::stopThread()
    {
        m_worker->exit(); // 使线程处理函数返回
        m_thread->quit(); // 使QThread的run()函数返回
        m_thread->wait(1000); // 等待线程处理完成
    }
    
  • wait():使程序阻塞指定的时间。由于quit()函数是非阻塞的,而线程清理一些资源是需要花费一些时间的。所以通常会在调用quit()后调用wait()来使程序等待一段时间。但是这不能作为线程结束的可靠依据。线程结束还是需要通过QThread的finished信号来处理。

3 线程同步(安全)

当一个变量或对象需要在不同的线程之间被使用时,则就可能存在同步问题。如:

void SubThread::run()
{
   
   
    qDebug() << "sub thread start";
    qDebug() << "sub thread id=" << QThread::currentThreadId();
    m_exit = false;
    int i = 0;
    while (!m_exit && i <= 100000)
    {
   
   
        if (i % 100 == 0)
        {
   
   
            m_count++;
            qDebug() << i << " ***  sub thread is running...." << m_count;
        }
        i++;
    }
    qDebug
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值