zoukankan      html  css  js  c++  java
  • 【QT】QThread源码浅析

    作者:李春港
    出处: https://www.cnblogs.com/lcgbk/p/13940142.html

    本章会挑出QThread源码中部分重点代码来说明QThread启动到结束的过程是怎么调度的。其次因为到了Qt4.4版本,Qt的多线程就有所变化,所以本章会以Qt4.0.1和Qt5.6.2版本的源码来进行浅析。

    一、QThread类的定义源码

    Qt4.0.1版本源码:

    #ifndef QT_NO_THREAD
    class Q_CORE_EXPORT QThread : public QObject
    {
    public:
        ...//省略
        explicit QThread(QObject *parent = 0);
        ~QThread();
        ...//省略
        void exit(int retcode = 0);
    
    public slots:
        void start(QThread::Priority = InheritPriority); //启动线程函数
        void terminate(); //强制退出线程函数
        void quit(); //线程退出函数
        ...//省略
    signals:
        void started(); //线程启动信号
        void finished(); //线程结束信号
        ...//省略
        
    protected:
        virtual void run() = 0;
        int exec();
        ...//省略
    };
    #else // QT_NO_THREAD
    

    Qt5.6.2版本源码:

    #ifndef QT_NO_THREAD
    class Q_CORE_EXPORT QThread : public QObject
    {
        Q_OBJECT
    public:
        ...//省略
        explicit QThread(QObject *parent = Q_NULLPTR);
        ~QThread();
        ...//省略
        void exit(int retcode = 0); //线程退出函数
        ...//省略
    public Q_SLOTS:
        void start(Priority = InheritPriority); //启动线程函数
        void terminate(); //强制退出线程函数
        void quit(); //线程退出函数
        ...//省略
    Q_SIGNALS:
        void started(QPrivateSignal); //线程启动信号
        void finished(QPrivateSignal); //线程结束信号
        
    protected:
        virtual void run();
        int exec();
        ...//省略
    };
    #else // QT_NO_THREAD
    

    从以上两个版本的代码可以看出,这些函数在声明上基本没什么差异,但是仔细看,两个版本的 run() 函数声明的是不是不一样?

    • Qt4.0.1版本run() 函数是纯虚函数,即此类为抽象类不可以创建实例,只可以创建指向该类的指针,也就是说如果你需要使用QThread来实现多线程,就必须实现QThread的派生类并且实现 run() 函数;
    • Qt5.6.2版本的run() 函数是虚函数,继承QThread类时,可以重新实现 run() 函数,也可以不实现。

    注:我查看了多个Qt版本的源码,发现出现以上差异的版本是从Qt4.4开始的。从Qt4.4版本开始,QThread类就不再是抽象类了。

    二、QThread::start()源码

    再来看看QThread::start()源码,Qt4.0.1版本和Qt5.6.2版本此部分的源码大同小异,所以以Qt5.6.2版本的源码为主,如下:

    void QThread::start(Priority priority)
    {
        Q_D(QThread);
        QMutexLocker locker(&d->mutex);
     
        if (d->isInFinish) {
            locker.unlock();
            wait();
            locker.relock();
        }
     
        if (d->running)
            return;
            
        ... ... // 此部分是d指针配置
     
    #ifndef Q_OS_WINRT
    
        ... ... // 此部分为注释
        
        d->handle = (Qt::HANDLE) _beginthreadex(NULL, d->stackSize, QThreadPrivate::start,
                                                this, CREATE_SUSPENDED, &(d->id));
    #else // !Q_OS_WINRT
        d->handle = (Qt::HANDLE) CreateThread(NULL, d->stackSize, (LPTHREAD_START_ROUTINE)QThreadPrivate::start,
                                                this, CREATE_SUSPENDED, reinterpret_cast<LPDWORD>(&d->id));
    #endif // Q_OS_WINRT
     
        if (!d->handle) {
            qErrnoWarning(errno, "QThread::start: Failed to create thread");
            d->running = false;
            d->finished = true;
            return;
        }
     
        int prio;
        d->priority = priority;
        switch (d->priority) {
        
        ... ... // 此部分为线程优先级配置
        
        case InheritPriority:
        default:
            prio = GetThreadPriority(GetCurrentThread());
            break;
        }
     
        if (!SetThreadPriority(d->handle, prio)) {
            qErrnoWarning("QThread::start: Failed to set thread priority");
        }
     
        if (ResumeThread(d->handle) == (DWORD) -1) {
            qErrnoWarning("QThread::start: Failed to resume new thread");
        }
    }
    

    挑出里面的重点来说明:

    (1)Q_D()宏定义

    在看源码的时候,当时比较好奇start函数的第一条语句 Q_D()宏定义 是什么意思,所以就看了下源码,在此也顺便讲讲,Q_D() 源码是一个宏定义,如下:

    #define Q_D(Class) Class##Private * const d = d_func()
    

    此处利用了预处理宏里的 ## 操作符:连接前后两个符号,变成一个新的符号。将Q_D(QThread)展开后,变成:QThreadPrivate * const d = d_func()。

    (2)_beginthreadex()函数
    上面d->handle = (Qt::HANDLE) _beginthreadex ( NULL, d->stackSize, QThreadPrivate::start, this, CREATE_SUSPENDED, &( d->id ) ) 语句中的函数是创建线程的函数,其原型以及各参数的说明如下:

    unsigned long _beginthreadex( 
     
    void *security,       // 安全属性,NULL为默认安全属性
     
    unsigned stack_size,  // 指定线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0
     
    unsigned ( __stdcall *start_address )( void * ), 
                          // 指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)
     
    void *arglist,        // 传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针
                            //如果传入this,这个this表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身
     
    unsigned initflag,    // 线程初始状态,0:立即运行;CREATE_SUSPEND:suspended(悬挂)
     
    unsigned *thrdaddr    // 用于记录线程ID的地址
     
    );
    

    三、QThreadPrivate::start()源码

    从QThread::start()源码可以知道,QThreadPrivate::start是重点,其实际就是调用了QThreadPrivate::start(this),这个 this 表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身。因为两个Qt版本此部分的源码大同小异,所以本部分主要是以5.6.2版本的源码为主,其源码以及说明如下:

    // 参数arg就是上面所说的this
    unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg)
    {
        QThread *thr = reinterpret_cast<QThread *>(arg);
        QThreadData *data = QThreadData::get2(thr);
     
        // 创建线程局部存储变量,存放线程id
        qt_create_tls();
        TlsSetValue(qt_current_thread_data_tls_index, data);
        data->threadId = reinterpret_cast<Qt::HANDLE>(quintptr(GetCurrentThreadId()));
     
        QThread::setTerminationEnabled(false);
     
        {
            QMutexLocker locker(&thr->d_func()->mutex);
            data->quitNow = thr->d_func()->exited;
        }
     
        if (data->eventDispatcher.load()) // custom event dispatcher set?
            data->eventDispatcher.load()->startingUp();
        else
            createEventDispatcher(data);
            
        ...//省略
        
        emit thr->started(QThread::QPrivateSignal()); // 发射线程启动信号
        QThread::setTerminationEnabled(true);
        thr->run(); // 调用QThread::run()函数 -- 线程函数
     
        finish(arg); //结束线程
        return 0;
    }
    

    由上述源码可以看出,实际上 run() 函数是在这里调用的,并且发出了 started() 启动信号,等到 run() 函数执行完毕,最后是调用了 QThreadPrivate::finish 函数结束线程,并且在finish内会发出 QThread::finished() 线程已结束的信号。

    四、QThread::run()源码

    再看看QThread::run()函数的源码。在上面 《2.1 QThread类的定义源码》的小节,我们可以看到两个Qt版本声明此方法的方式不一样,Qt-4.0版本将此定义为了纯虚函数,而Qt-5.6版本将此定义为了虚函数,那我们就看看Qt-5.6版本中,QThread::run()是如何定义的,如下:

    void QThread::run()
    {
        (void) exec();
    }
    
    1. 每一个 Qt 应用程序至少有一个 事件循环 ,就是调用了 QCoreApplication::exec() 的那个事件循环。不过,QThread也可以开启事件循环。只不过这是一个受限于线程内部的事件循环。因此我们将处于调用main()函数的那个线程,并且由 QCoreApplication::exec() 创建开启的那个事件循环成为 主事件循环 ,或者直接叫 主循环 。注意,QCoreApplication::exec()只能在调用main()函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,因为所有有关 GUI 的操作都必须在这个线程进行。QThread的局部事件循环则可以通过在 QThread::run() 中调用 QThread::exec() 开启。

    2. 我们通过以上源码可以看到,它的定义很简单,就是调用了一个函数:QThread::exec() 开启线程中的 事件循环 ,我们也可以通过继承QThread,重写run()函数的方式,让其实现相对复杂的逻辑代码。如果你的线程需要将某些槽函数在本线程完成的话,就必须开启事件循环,否则在线程内无法响应各种信号并作出相应的行为。

    小结: 比Qt-4.4版本更早的版本中,我们使用QThread启动线程时,就必须要实现继承于QThread的派生类,并且一定要重写run函数,若需要使用事件循环,就需要在run函数中添加exec()。到了Qt4.4版本之后(包括Qt4.4版本),QThread就不是抽象类了,不派生也可以实例化,在不重写QThread::run()方法,start启动线程是默认启动事件循环的。

    注:当程序跑到了exec()代码时,位于exec()后面的代码就不会再被执行,除非我们使用quit、exit等退出语句来退出事件循环,退出后,程序才会继续执行位于exec()后面的代码。

    五、QThread::quit()、QThread::exit()、QThread::terminate()源码

    线程停止函数的区别,从Qt源码来分析:

    (1)QThread::quit()、QThread::exit()

    //QThread::quit()声明
    void quit();
    //QThread::quit()定义
    void QThread::quit()
    { exit(); }
    
    //QThread::exit()声明
    void exit(int retcode = 0);
    //QThread::exit()定义
    void QThread::exit(int returnCode)
    {
        Q_D(QThread);
        QMutexLocker locker(&d->mutex);
        d->exited = true;
        d->returnCode = returnCode;
        d->data->quitNow = true;
        for (int i = 0; i < d->data->eventLoops.size(); ++i) {
            QEventLoop *eventLoop = d->data->eventLoops.at(i);
            eventLoop->exit(returnCode);
        }
    }
    

    由以上源码可知,QThread::quit()QThread::exit(0) 的调用是等效的,都是告诉线程的事件循环,以返回码0(成功)退出。如果线程没有事件,则此函数不执行任何操作,也就是无效的。当线程拥有事件循环并且正处于 事件循环(QThread::exec()) 的状态时,调用 QThread::quit()或者QThread::exit() 线程就会马上停止,否则不会立刻停止线程,直到线程处于事件循环也就是正在执行 QThread::exec() 时,才会停止线程。

    如果重复调用 QThread::quit()或者QThread::exit() 会有什么影响吗?
    重复调用 QThread::quit()或者QThread::exit() 也不会有什么影响,因为只有拥有事件循环的线程,这两个函数才会生效停止线程的功能。

    (2)QThread::terminate()

    void QThread::terminate()
    {
        Q_D(QThread);
        QMutexLocker locker(&d->mutex);
        if (!d->running)
            return;
        if (!d->terminationEnabled) {
            d->terminatePending = true;
            return;
        }
    
    // Calling ExitThread() in setTerminationEnabled is all we can do on WinRT
    #ifndef Q_OS_WINRT
        TerminateThread(d->handle, 0);
    #endif
        QThreadPrivate::finish(this, false); //结束线程函数
    }
    

    在这个函数定义的最后一个语句,是调用了 QThreadPrivate::finish(this, false); 函数,其函数作用是直接退出线程,无论线程是否开启了事件循环都会生效,会马上终止一个线程,但这个函数存在非常不安定因素,不推荐使用

    如果重复调用 QThread::terminate() 会有什么影响吗?
    没有影响。我们可以看到函数体里面的第三条语句,它首先会判断线程是否还在运行中,如果不是,会直接退出函数,就不会继续往下执行调用QThreadPrivate::finish(this, false); 函数了。

    六、章节小结

    相信看了以上的一些QThread源码,都大概知道了QThread类的本质以及QThread开启到结束的过程。这里我再简单总结下:

    (1)QThread的本质:

    • QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西;
    • QThread 所依附的线程,就是执行 QThread t 或 QThread * t=new QThread 所在的线程;
    • QThread 管理的线程,就是 run 启动的线程,也就是次线程。

    (2)在这里针对Qt4.4版本之后(包括Qt4.4版本)简单汇总一下线程启动到结束的过程:

    • QThread对象或者QThread派生类对象显式调用QThread类中的外部start()方法;
    • QThread::start()方法再调用QThreadPrivate::start()方法;
    • 在QThreadPrivate::start()方法内调用了QThread::run()虚函数,对使用者来说到了这里才是真正进入了一个新的线程里面。也就是说定义QThread对象或者QThread派生类对象的时候,还是在原来的线程里面,只有进入run函数才是进入了新的线程;
    • 在QThreadPrivate::start()方法调用QThread::run()虚函数结束后,就会继续调用QThreadPrivate::finish()函数来结束线程,并发出线程结束的信号finished()。

    (3)QThread::quit()、QThread::exit()、QThread::terminate():

    • 对线程重复使用这三个停止线程的函数,没有任何影响;
    • 尽量不要使用QThread::terminate()停止线程,此方式是强制退出线程,没有安全保障。
    • 调用QThread::quit()和QThread::exit()一样。

    (4)Qt各版本QThread类的变化:

    • Qt4.4版本之前QThread类是属于抽象类, Qt4.4版本之后(包括4.4版本)不是抽象类。
  • 相关阅读:
    数据终端设备与无线通信模块之间串行通信链路复用协议(TS27.010)在嵌入式系统上的开发【转】
    设备树网址【原创笔记】
    clock()、time()、clock_gettime()和gettimeofday()函数的用法和区别【转】
    ajaxFileUpload SyntaxError: syntax error
    工厂模式
    程序猿都是project师吗?
    [android开发之内容更新类APP]二、这几日的结果
    Java实现将指定目录内的指定类型的文件归类
    移动支付之智能IC卡与Android手机进行NFC通信
    Java并发框架——AQS堵塞队列管理(一)——自旋锁
  • 原文地址:https://www.cnblogs.com/lcgbk/p/13940142.html
Copyright © 2011-2022 走看看