QT中QThread的各个方法,UI线程关系,事件关系详解(3)

55 篇文章 26 订阅
37 篇文章 17 订阅

回顾Qt之线程(QThread),里面讲解了如何使用线程,但还有很多人留言没有看明白,那么今天我们来一起瞅瞅关于QThread管理线程的那些事儿。。。


一、线程管理

1、线程启动

void start(Priority priority = InheritPriority)

调用后会执行run()函数,但在run()函数执行前会发射信号started(),操作系统将根据优先级参数调度线程。如果线程已经在运行,那么这个函数什么也不做。优先级参数的效果取决于操作系统的调度策略。特别是那些不支持线程优先级的系统优先级将会被忽略(例如在Linux中,更多细节请参考sched_setscheduler(2) - Linux man page)。

2、线程执行

int exec()

进入事件循环并等待直到调用exit(),返回值是通过调用exit()来获得,如果调用成功则范围0。

virtual void run();

线程的起点,在调用start()之后,新创建的线程就会调用这个函数,默认实现调用exec(),大多数需要重新实现这个功能,便于管理自己的线程。该方法返回时,该线程的执行将结束。

3、线程退出

void quit()

告诉线程事件循环退出,返回0表示成功,相当于调用了QThread::exit(0)。

void exit(int returnCode = 0)

告诉线程事件循环退出。

调用这个函数后,线程离开事件循环后返回,QEventLoop::exec()返回returnCode,

按照惯例0表示成功,任何非0值表示失败。

void terminate()

终止线程,线程可能会立即被终止也可能不会,这取决于操作系统的调度策略,使用terminate()之后再使用QThread::wait()确保万无一失。

当线程被终止后,所有等待中的线程将会被唤醒。

警告:此功能比较危险,不鼓励使用。线程可以在代码执行的任何点被终止。线程可能在更新数据时被终止,从而没有机会来清理自己,解锁等等。。。总之,只有在绝对必要时使用此功能。

建议:一般情况下,都在run函数里面设置一个标识符,可以控制循环停止。然后才调用quit函数,退出线程。

4、线程等待

void msleep(unsigned long msecs)

强制当前线程睡眠msecs毫秒

void sleep(unsigned long secs)

强制当前线程睡眠secs秒

void usleep(unsigned long usecs)

强制当前线程睡眠usecs微秒

bool wait(unsigned long time = ULONG_MAX);

线程将会被阻塞,等待time毫秒。和sleep不同的是,如果线程退出,wait会返回。

5、线程状态

    bool isFinished() const    

    线程是否结束

    bool isRunning() const    

    线程是否正在运行

6、线程优先级

    void setPriority(Priority priority)

    这个函数设置正在运行线程的优先级。如果线程没有运行,此功能不执行任何操作并立即返回。使用的start()来启动一个线程具有特定的优先级。

    优先级参数可以是QThread::Priority枚举除InheritPriortyd的任何值。

    Priority priority() const

    下面来看下优先级中的各个枚举值:

Constant

Value

Description

QThread::IdlePriority

0

没有其它线程运行时才调度.

QThread::LowestPriority

1

比LowPriority调度频率低.

QThread::LowPriority

2

比NormalPriority调度频率低.

QThread::NormalPriority

3

操作系统默认的默认优先级.

QThread::HighPriority

4

比NormalPriority调度频繁.

QThread::HighestPriority

5

比HighPriority调度频繁.

QThread::TimeCriticalPriority

6

尽可能频繁的调度.

QThread::InheritPriority

7

使用和创建线程同样的优先级. 这是默认值.

 

二、主线程、次线程

Qt之线程(QThread)一节中我介绍了QThread 的两种使用方法:

1、子类化 QThread(不使用事件循环)。

这是官方手册、例子以及相关书籍中都介绍的一种常用的方法。

a. 子类化 QThread

b. 重载 run 函数,run函数内有一个while或for的死循环(模拟耗时操作)

c. 设置一个标记为来控制死循环的退出。

2、子类化 QObject

a. 子类化 QObject

b. 定义槽函数

c. 将该子类的对象moveToThread到新线程中

run 对于线程的作用相当于main函数对于应用程序。它是线程的入口,run的开始和结束意味着线程的开始和结束。

采用这两种做法,毫无疑问都会在次线程中运行(这里说的是,run中的逻辑以及子类化QObject后连接通过moveToThread然后连接到QThread的started()信号的槽函数,这个下面会详细讲解)。

那么,线程中的槽函数是怎么运行的呢?

说到信号与槽,大家应该再熟悉不过了,包括我,特别喜欢使用自定义信号与槽,感觉用起来特方便、特棒。。。

经常使用,你能否100%的使用正确?你了解它的高级用法吗?

1、你是否在多次connect,还发现不了为什么槽函数会执行那N多次。

2、你是否了解disconnect

3、你是否了解connect中的第五个参数 Qt::ConnectionType

关于connect、disconnect信号、槽的使用可参考:Qt之信号与槽。既然谈到线程这里需要重点说下Qt::ConnectionType(信号与槽的传递方式)

Constant

Value

Description

Qt::AutoConnection

0

自动连接:(默认值)如果信号在接收者所依附的线程内发射,则等同于直接连接。如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接。

Qt::DirectConnection

1

直接连接:当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。

Qt::QueuedConnection

2

队列连接:当控制权回到接受者所依附线程的事件循环时,槽函数被调用。槽函数在接收者所依附线程执行。也就是说:这种方式既可以在线程内传递消息,也可以跨线程传递消息

Qt::BlockingQueuedConnection

3

与Qt::QueuedConnection类似,但是会阻塞等到关联的slot都被执行。这里出现了阻塞这个词,说明它是专门用来多线程间传递消息的。

举例:

MyObject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include 

class MyObject : public QObject
{
    Q_OBJECT

public:
    explicit MyObject(QObject *parent = 0);

public slots:
    void start();
};

#endif // MYOBJECT_H
MyObject.cpp
#include "MyObject.h"
#include 
#include 

MyObject::MyObject(QObject *parent)
    : QObject(parent)
{

}

void MyObject::start()
{
    qDebug() << QString("my object thread id:") << QThread::currentThreadId();
}
main.cpp
#include "MyObject.h"
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    qDebug() << QString("main thread id:") << QThread::currentThreadId();

    MyObject object;
    QThread thread;
    object.moveToThread(&thread);
    QObject::connect(&thread, SIGNAL(started()), &object, SLOT(start()));
    thread.start();

    return a.exec();
} 

查看运行结果:

  "main thread id:" 0xf08

  "my object thread id:" 0x216c

    显然主线程与槽函数的线程是不同的(你可以多次尝试,屡试不爽。。。),因为moveToThread后MyObject所在的线程为QThread,继上面介绍的thread.start()执行后首先会发射started()信号,也就是说started()信号发射是在次线程中进行的,所以无论采取Qt::AutoConnection、Qt::DirectConnection、Qt::QueuedConnection哪种连接方式,主线程与槽函数的线程都是不同的。

1、修改代码如下:

    MyObject object;
    QThread thread;
    //object.moveToThread(&thread);
    QObject::connect(&thread, SIGNAL(started()), &object, SLOT(start()), Qt::DirectConnection);
    thread.start();

查看运行结果:

  "main thread id:" 0x2688

  "my object thread id:" 0x2110 

    显然主线程与槽函数的线程是不同的,MyObject所依附的线程为主线程(因为注释掉了moveToThread),继上面介绍的Qt::DirectConnection(无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行)。也就是说started()信号发射是在次线程中进行的,槽函数也是在次线程中进行的,所以主线程与槽函数的线程是不同的。

2、修改代码如下:

    MyObject object;
    QThread thread;
    //object.moveToThread(&thread);
    QObject::connect(&thread, SIGNAL(started()), &object, SLOT(start()), Qt::QueuedConnection);
    thread.start();

查看运行结果:

  "main thread id:" 0x24ec

  "my object thread id:" 0x24ec 

    显然主线程与槽函数的线程是相同的,继上面介绍的Qt::QueuedConnection(槽函数在接收者所依附线程执行)。也就是说started()信号发射是在次线程中进行的,但MyObject所依附的线程为主线程(因为注释掉了moveToThread),所以主线程与槽函数的线程必然是相同的。

3、修改代码如下:

    MyObject object;
    QThread thread;
    //object.moveToThread(&thread);
    QObject::connect(&thread, SIGNAL(started()), &object, SLOT(start()), Qt::AutoConnection);
    thread.start();

查看运行结果:

  "main thread id:" 0x2700

  "my object thread id:" 0x2700 

    显然主线程与槽函数的线程是相同的,MyObject所依附的线程为主线程(因为注释掉了moveToThread),继上面介绍的Qt::AutoConnection(如果信号在接收者所依附的线程内发射,则等同于直接连接。如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接。)。因为started()信号和MyObject依附的线程不同,所以结果和Qt::QueuedConnection对应的相同,所以主线程与槽函数的线程是相同的。

    基本就介绍到这里,QThread使用和上面的大同小异,run里面执行的代码都是在次线程中,如果是QThead的槽函数,那么结论同上!

/*******************************************************************

QThread必须要了解的几个函数

概述
    如果想对Qt中的QThread有个更加深刻的了解,必须要知道这几个重要的函数,现在就一一介绍下。

函数介绍
属性    返回值    函数体    功能
static    QThread *    QThread::currentThread()        返回当前线程的指针,静态函数。
static    Qt::HANDLE    QThread::currentThreadId()        返回当前线程的句柄,静态函数
static    bool    QThread::isFinished() const    如果线程执行结束,返回true,否则返回false
static    bool    QThread::isRunning() const    如果当前线程在运行中,返回true,否则返回false
static    int    QThread::idealThreadCount()        返回理理想状态下该系统支持的线程的数量。如果无发现检测到处理器的核数,返回值为-1
protected    int    exec()        使线程进入事件循环状态,并且处于wait状态,直到调用exit()函数使其退出。退出时返回值是调用exit()函数时的输入参数。如果调用quit()函数,其退出的返回值为0.该函数一般在run()函数中调用,使线程进入事件循环处理状态。
protected    void    exit(int returnCode = 0)        告知线程从事件循环状态退出,并且返回returnCode的值。一般说来,返回0表示成功退出,返回非0值表示遇到错误。调用该函数后,线程不会再进行事件处理,除非再次调用exec()函数。如果当前线程不处于执行状态,那么下次调用exec()也会直接返回
private signal    void    finished()        在线程执行完毕前发出该信号,当发出该信号时,意味着线程早已经退出了事件循环处理状态,即不再处理除了延迟删除事件(deferred deletion)之外的任何事件。可以把该信号和QObject::deleteLater()连接起来用来删除线程中的对象。如果强制使用terminate()函数来结束线程,那么将无法得知finish()信号的发送线程。另外,这是一个私有信号,所以用户无法发出这个信号。
static    bool    QThread::isInterruptionRequested() const        返回当前线程上运行的任务是否可以停止,可以通过requestInterruption()来进行中断查询。该函数可以使得长时间运行的任务彻底停止,永远不要检查该函数的返回值是否是安全的。但是进行长时间任务时提倡有规律的使用该方法。但是不要频繁使用该方法避免线程切换的开销。
static    bool    QThread::isRunning() const        返回当前线程是否正在运行,如果在运行,返回true,否则返回false
static    int    QThread::loopLevel() const        该函数返回当前事件循环的层级,但是该函数只能在线程内部调用。
static    void    QThread::msleep(unsigned long msecs)        强制线程休息msecs毫秒
static    void    QThread::usleep(unsigned long usecs)        强制线程休息usecs微秒
static    void    QThread::msleep(unsigned long secs)        强制线程休息secs秒
virtual protected    void    QThread::run()        该函数是线程运行的起始点。调用start()函数之后,新创建的函数调用该函数,默认的QThread简单的调用exec()进入事件循环机制。我们可以重载这个函数来实现更高级的线程管理方式。从该函数返回将结束线程。
static    void    QThread::setPriority(Priority priority)        该函数设置线程运行的优先级,如果线程并没有处于运行状态,该函数什么也不做。可以通过调用start()来指定一个优先级来启动线程。优先级的效果依赖于操作系统的调度方式,尤其是在不支持优先级的操作系统上,优先级设置会被忽略。
slot    void    QThread::quit()        告诉线程的事件循环退出并且返回0值,相当于调用QThread::exit(0)。如果线程没有事件循环,这个函数则什么也不做。
virtual protected    void    QThread::setStackSize(uint stackSize)        设置线程堆栈的最大值,如果设置的值大于0,那么就会将堆栈最大值设置为当前数值。否则,线程堆栈的最大值由操作系统来决定。警告:大部分操作系统都会自己设置线程堆栈的最大最小值,如果设置的堆栈大小超出范围,线程则不能启动。
static protected    void    QThread::setTerminationEnabled(bool enabled = true)    
slot    void    QThread::start(Priority priority = InheritPriority)        通过调用run()函数启动线程。操作系统会根据优先级来调度线程。如果线程已经处于运行状态,该函数什么也不做。优先级设置依赖于操作系统的线程调度方式。
signal    void    QThread::started()        线程开始执行时发出该信号,发出时间在run()函数调用之前。注意:这是一个私有信号,因此只能由线程发出,用户不能发出该信号。
slot    void    QThread::terminate()        终止当前线程。线程或许不会立即被终止,依赖于线程的调度策略。一般情况下,调用该函数之后再调用QThread::wait()来确保线程结束。该线程终止后,等待该线程的其他线程将被唤醒。警告:该函数比较危险,不推荐这样做。线程可能在任何代码处终止。或许在修改数据时被终止,线程结束后自己不能去做清理工作。
static    bool    QThread::wait(unsigned long time = ULONG_MAX)        阻塞当前的进程,直到满足如下两个条件之一: 1.相关的线程完成其任务,然后如果线程已经结束,则该函数返回true,如果线程没有启动,则该函数也会返回true。 2. 经过了特定长度的时间,如果时间是ULONG_MAX(默认值),那么wait()函数几乎不会超时。(即该函数必须从run()函数返回)如果wait函数超时,那么该函数会返回false。
static    void    QThread::yieldCurrentThread()    将当前线程的执行权让给别的可执行线程。至于让给哪一个可执行线程,那就是操作系统的事情了。
使用技巧
一般调用quit()函数之后可以紧接着调用wait()函数确保线程退出。
sleep()等让线程休眠的函数不需要调用,因为Qt中线程是事件驱动机制。但是如果是继承的QTHread类,在run()函数中使用了无限循环的方式,可以考虑msleep()函数来使线程休息一段时间,一般为1毫秒。
/******************************************************************************

Qt对话框的事件循环分析(子线程中不能创建UI窗体分析)

重要:

GUI线程和辅助线程
如前所述,每个程序在启动时都有一个线程。这个线程被称为“主线程”(在Qt应用程序中也称为“GUI线程”)。Qt GUI必须在这个线程中运行。所有小部件和几个相关类(例如QPixmap)都不能在辅助线程中工作。辅助线程通常称为“工作线程”,因为它用于从主线程卸载处理工作。

首先,子线程不能创建与UI有关的对象,但是可以这样子做.只能在子线程中发一个信号到主线程中,由主线程创建对话窗口.子线程发完信号后,在子线程中while循环调用事件循环,.对话窗口退出之后,主线程调用接口,结束子线程的while循环!

默认的线程在Qt中称为窗口线程,也叫主线程(UI线程),负责窗口事件处理或者窗口控件数据的更新
子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情都要交给窗口线程处理
主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制。
/*************************************************

Qt只允许您在GUI线程中创建GUI元素

不能在子线程中操作UI,可以在子线程向主线程发送信号,主线程对应的槽函数来处理UI

其实Qt只是封装了Win32的Api,底层还是Win32的那套东西。
MFC/Win32一般情况下只有一个界面线程(可以理解为主线程,拥有消息队列),可以有N多工作线程。
而QThread应该指的是工作线程。
所以一个简单的QT-GUI程序,在Windows下应该是只有一个界面线程。

一、澄清概念

1.Qt主线程

Qt的主线程是唯一运行创建QApplication对象并调用exec()的线程,主要用于界面显示,因此又被称为GUI线程。

2.Qt子线程

Qt的子线程用于一些耗时操作,因此又被称为工作线程。

子线程不能用于直接刷新界面(QWidget不可重入,QObject可重入)。

若子线程企图修改界面控件,可通过线程间通信的方式:Qt的信号槽机制是跨线程的,因此可以用作线程间通信。

主线程是唯一允许创建QApplication或者QCoreApplication对象的,并且调用exec()。exec()启动了事件循环,一直在等待接收并且处理一个个Qt封装好的事件,比如鼠标移动事件,键盘按下事件等等。所以只有在主线程里你才可以方便利用各种Event去完成自己想要实现的需求。所以就限制你必须在主线程作UI相关操作。
除了规定,往根源说,再多的线程,实质上对于CPU来说,也是一件一件的处理,并不是我们凭空现象的同时处理。只是可以“智能”的处理一下当前迫切需要的数据,然后可以随时暂停,再去处理更加迫切的。如果2个线程同时处理UI显示,一个线程正在用于和用户交互更新显示,另外一个线程就只能是等待状态,并不能完成我们所期望的同时刷新UI的期望。
所以,基本上大部分应用开发框架都限制更新、创建UI必须在主线程里完成,而逻辑运算可以匹出新线程去完成。

t是一个以事件为驱动的框架。

QTUI在主线程,Qt所有的事件都在UI主线程, 包括定时事件。所有所有耗时的操作务必不要在事件中处理,否则影响整个的刷新的时间。。。。。

可以使用线程把去处理复杂的事件,然后把结果传到UI线程中

线程绘图

功能:子线程在处理函数中绘制图像,然后通过信号把绘制的图像传给主线程,主线程接收到图像之后调用update()函数更新绘图事件,进行图像的绘制。

也就是子线程把图片给画好了,传给主线程,主线程在窗口中绘制出来。主窗口中有一个按钮,按一次,绘制一次图像。

/*********************************************

1. 线程与界面组件需要注意的地方

在QThread线程中不能直接创建QWidget之类的界面组件.
因为在QT中,所有界面组件相关的操作都必须在主线程中(也就是GUI thread)
所以, QThread线程不能直接操作界面组件.
2.QThread线程如何操作界面组件-方法1

将多线程类对象封装为GUI界面类的类成员
然后在子线程定义信号函数,通过信号槽机制,向界面组件emit发射信号,从而实现间接操作.
3.QThread线程如何操作界面组件-方法2

使用QApplication::postEvent()实现向界面发送事件,从而能够封装一个自定义类
4.使用Invokes()函数来调用界面组件的信号槽-方法3

一般使用该函数(用来调用对方的私有信号或槽):

该函数的连接方式默认使用的是Qt::AutoConnection

表示如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
比如,当我们想调用一个obj下的compute(QString, int, double)槽函数时:

则只需要写入:

QMetaObject::invokeMethod(obj, "compute",
                            Q_ARG(QString, "sqrt"),                        
                            Q_ARG(int, 42),
                            Q_ARG(double, 9.7));
示例如下所示:

在Testtherd线程类里通过invokeMethod向父界面类的paintMsg槽函数发送信息

void Testtherd::run()
{
    int count=0;
    while(1)
    {
        QString str="请稍等,正在验证用户,登录中";
        for(int i =0;i<count;i++)
           str.append('.');                   //循环添加小数点
        count=(count+1)%7;
        QMetaObject::invokeMethod(this->parent(), "paintMsg",
                                            Q_ARG(QString, str));

        msleep(500);
    }
}


父界面类的paintMsg槽函数如下所示:

void loginwindow:: paintMsg(QString msg) {
this->LineHint->setText(msg);
}
运行效果如下:

/*********************************************

写下这个给自己备忘,关于事件循环以及多线程方面的东西我还需要多多学习。首先我们都知道程序有一个主线程,在GUI程序中这个主线程也叫GUI线程,图形和绘图相关的函数都是由主线程来提供。主线程有个事件循环Event Loop,其实就是一个死循环在不断的等待你的消息队列,通过消息队列完成响应用户操作,绘图,以及相关操作。我们都知道QDialog有一个exec函数,这个函数会形成“模态”对话框,然后等待用户去输入OK还是Cancel,否则他绝不返回,如下

void test()
{
    QDialog dialog;
    dialog.exec();

    qDebug() << "finish exec";
}


我们可以看这个简单的例子,可以看到,当dialog被exec之后,我们的qDebug是不会输出的,除非我们人为去点了对话框的OK,否则,就会一直卡在exec之上。这个时候可能同学会有我一开始一样的误解,我们会误以为此时事件循环停止了,其实并不是停止,而是阻塞住了。为什么会阻塞?因为test这个函数没有返回嘛!

m_stateManager->postEvent(ev);
投递事件官方API也说的很清楚,会立即返回,所以别去担心此时投递的事件进入不到消息队列,真正要关心的是此时dialog的exec让你的主线程阻塞了,这个时候消息队列上的事件都不会进行操作,都在等待dialog的返回,只有dialog返回,接下来的事件才会依次进行。要记住,消息是可以正常投递的。

那么,有没有办法可以让dialog.exec()立即返回,同时我的对话框还在呢?方法是有的

void test()
{
    metaObject()->invokeMethod(this, "invokeTest", Qt::QueuedConnection);

    qDebug() << "finish exec";
}

Q_INVOKABLE void invokeTest()
{
    QDialog dialog;
    dialog.exec();
}


答案就是用Qt提供的元对象系统Meta Object System的invokeMethod,并且将第三个参数设为Qt:QueuedConnection。从字面上我们也可以看的出来,这个调用不会去立即调用,相反他是异步的,他会把这个函数投递到事件队列里,也就是说,这个例子中的qDebug会输出,输出之后,事件队列才会去调用dialog.exec()这个函数,当然了,调用这个函数之后又会达到我们一开始的那个阻塞的效果,你通过异步到最后的触发,始终你需要去面对exec给你当前事件循环造成阻塞的问题。

让我们再深入一点,当一个事件循环的时候还算简单。但是我们知道,Qt中对于状态机他是有一个异步的事件循环的,也就是说外面有事件循环,状态机本身也有事件循环。比如

m_stateManager->postEvent(ev);

m_tool->run();
你给状态机投递了一个事件,他根据状态迁移去调用你的tool,这一切看起来很美好,但如果你此时的tool是个跟之前一样的阻塞的exec呢?让我们来看一下。

void Tool::run()
{
    QDialog dialog;
    dialog.exec();
}
对于这个情况,当我们运行tool之后,我们的状态机就跟之前的主事件事件循环一样被阻塞了,也就是说如果我此时继续

m_stateManager->postEvent(ev2);
和之前一样,这个postEvent会立即返回,因为投递到事件队列都是立即返回的。但是关键的问题在于你的状态机整个事件循环都停止不动了,都在等你之前的tool运行结束,但因为你之前的tool是个dialog.exec()你必须手动去点OK,不然你的状态机事件循环就阻塞不动了,这个时候如果你的客户不断的去点你这个tool的event,那会产生噩梦般的效果----你点完OK之后又会来OK之后又会来OK。。。这其实就是你一旦点了OK,你的消息队列就又可以循环了,之前等待的ev就都会去执行了。而且要注意的就是,此时你的exec的执行在主线程上,只是不能进行返回,但还是可以接收诸如键盘,鼠标等事件投递。

前面也说了,事件循环和状态机循环是两个独立的循环,其实这也很好理解。如果没有事件循环,状态机事件怎么知道你有没有按下这个键?从而去投递给状态机呢?其实也就是说当你状态机事件阻塞的时候,你的主事件循环还在不断的接收你的键盘和鼠标的操作,这一点是没有影响的。

因此,要想实现在tool的时候我还能相应别的状态事件,其实做法也是一样的,就是

void Tool::run()
{
    metaObject()->invokeMethod(this, "invokeTest", Qt::QueuedConnection);
}

Q_INVOKABLE void invokeTest()
{
    QDialog dialog;
    dialog.exec();
}


立即返回,这个”立即返回“并不是说你的事情做完了,而是你更想让状态机能够进行之后事件的循环,别去因为你的dialog而耽误了大家。

最后说说模态这个主题,其实模态的理解就是你的消息队列都在正常进行,因为你不断的在等待,导致事件循环不能进行下去,必须你这边正常返回,你接下来的操作才能继续。

今天又重新思考了一下这个问题。同步的意思似乎就是必须要执行完成才能返回。异步的概念就是立即返回,之后执行,会把他扔到消息队列里,待同步函数处理完之后,然后去搜索事件队列进行操作。其实状态机归根结底就是一堆信号链接,只是他的方向是规矩状态迁移表来进行。作为主线程的Event Loop来说,当dialog进入exec的时候,就是就是在进行事件队列,并不是说他此时把事件队列给阻塞了,这个我之前理解有问题。exec的含义就是去处理事件队列,去处理事件循环,去检索当前还有哪些事件可以被处理,从而去正常处理。比如我们有一个主程序窗口MainWindow,有一个Dialog,此时我们去调用dialog的exec,内部会去创建一个QEventLoop,又因为这个dialog的所在线程和MainWindow在同一个线程上,所以看上去似乎是两个EventLoop,但实际上都是同一个线程的Event Loop(一个线程只能有一个Event Loop,这是原子性问题)。所以在dialog进行exec的时候他会去检索主线程上的事件队列去操作。

而我们之前讲的状态机,其实仔细想了想很简单,你就把他理解成是主程序总的Event Loop中的一个事件,他在进行操作的时候,不返回(tool去调用dialog的exec看上去似乎进行了事件循环在等待你新的event,但别忘了,你本身这个tool的run就是通过事件队列去触发的)所以必须要等待这个tool的exec返回,你的事件队列才能正常下去。

再次强调:

exec并不是说事件队列被你阻塞,而是才是让你进入一个真正等待处理事件队列的过程。
同一个线程只能有一个Event Loop,这个可以参考CP单核心单线程的处理逻辑。
在进行事件队列进行事件操作的时候,其实内部就是同步的方式在进行,必须等待函数全部执行完毕才能真正返回才能真正进行之后的event,这也可以说的通我们之前举的状态机的例子,看上去这个状态机引发了我们的tool,tool中调用了dialog的exec,看上去似乎很美好,在等待状态事件了。但此时你这个exec不返回,你如何让事件Event Loop继续进行下去。
同步函数就是必须要等待函数操作完成之后才能返回的函数。异步函数就是直接返回,他具体什么时候进行操作,待具体实现查看。(也可能是本线程的事件队列,也可能是别的线程进行run)。如果是别的线程进行run的时候,你可能会去想这个立即返回的问题,其实很简单,Qt的run都是start,其实就跟postEvent一样,只是简单的把他注册给线程管理器,由线程管理器再去跑他的run函数,那你本地的start当然立即返回了。

/******************************************************

什么是线程?
线程就是并行地做事情,就像进程一样。那么线程与进程有什么不同呢?当您在电子表格上进行计算时,可能还会有一个媒体播放器在同一台式机上运行,播放您最喜欢的歌曲。下面是两个并行工作进程的例子:一个运行电子表格程序;一个经营媒体播放器。多任务处理是一个众所周知的术语。仔细观察媒体播放器就会发现,在一个单一的过程中,也有一些事情是并行发生的。当媒体播放器将音乐发送到音频驱动程序时,用户界面的所有铃声和哨子都在不断更新。这就是线程的作用所在,单个进程中的并发性。

那么并发性是如何实现的呢?在单核cpu上并行工作是一种错觉,有点类似于电影中移动图像的错觉。对于进程,这种错觉是通过在很短的时间后中断处理器在一个进程上的工作而产生的。然后处理器转到下一道工序。为了在进程之间切换,保存当前的程序计数器并加载下一个处理器的程序计数器。这是不够的,因为同样需要对寄存器、某些体系结构和操作系统特定的数据进行处理。

正如一个CPU可以驱动两个或多个进程一样,也可以让CPU运行在一个进程的两个不同代码段上。当一个进程启动时,它总是执行一个代码段,因此这个进程被称为有一个线程。然而,程序可能决定启动第二个线程。然后,在一个进程内同时处理两个不同的码序列。通过重复保存程序计数器和寄存器,然后加载下一个线程的程序计数器和寄存器,在单核cpu上实现并发。在活动线程之间循环不需要程序的合作。当切换到下一个线程时,一个线程可能处于任何状态。

当前CPU设计的趋势是多核并存。典型的单线程应用程序只能使用一个内核。然而,一个具有多个线程的程序可以被分配到多个核上,使事情以真正并发的方式发生。因此,将工作分配给多个线程可以使程序在多核cpu上运行得更快,因为可以使用额外的核。

GUI线程和辅助线程
如前所述,每个程序在启动时都有一个线程。这个线程被称为“主线程”(在Qt应用程序中也称为“GUI线程”)。Qt GUI必须在这个线程中运行。所有小部件和几个相关类(例如QPixmap)都不能在辅助线程中工作。辅助线程通常称为“工作线程”,因为它用于从主线程卸载处理工作。

同时存取数据
每个线程都有自己的堆栈,这意味着每个线程都有自己的调用历史和局部变量。与进程不同,线程共享相同的地址空间。下图显示了线程的构建块是如何位于内存中的。非活动线程的程序计数器和寄存器通常保存在内核空间中。每个线程都有一个共享的代码副本和一个单独的堆栈。


如果两个线程有一个指向同一对象的指针,那么两个线程可能会同时访问该对象,这可能会破坏对象的完整性。很容易想象,当同一个对象的两个方法同时执行时,可能会出现许多问题。

有时需要从不同的线程访问一个对象;例如,生活在不同线程中的对象需要通信时。由于线程使用相同的地址空间,因此线程交换数据比进程交换数据更容易、更快。数据不必序列化和复制。传递指针是可能的,但必须严格协调哪个线程接触哪个对象。必须防止同时在一个对象上执行操作。有几种方法可以实现这一点,下面将介绍其中一些方法。

那么什么是安全的呢?在一个线程中创建的所有对象都可以在该线程中安全地使用,前提是其他线程没有对它们的引用,并且对象与其他线程没有隐式耦合。这种隐式耦合可能发生在实例之间共享数据时,比如静态成员、单例或全局数据。熟悉线程安全类和可重入类和函数的概念。

使用线程
线程基本上有两个用例:

利用多核处理器提高处理速度。
通过卸载长时间持续的处理或阻塞对其他线程的调用来保持GUI线程或其他时间关键线程的响应。
何时使用线程的替代方案
开发人员需要非常小心线程。启动其他线程很容易,但很难确保所有共享数据保持一致。问题通常很难发现,因为它们可能只是偶尔出现,或者只在特定的硬件配置上出现。在创建线程来解决某些问题之前,应该考虑可能的替代方案。

供选择的    注释
QEventLoop::processEvents()    在耗时的计算过程中反复调用QEventLoop::processEvents()可以防止GUI阻塞。但是,这个解决方案不能很好地扩展,因为根据硬件的不同,对processEvents()的调用可能发生得太频繁,或者不够频繁
QTimer    后台处理有时可以方便地使用一个计时器来调度一个插槽在未来的某个时间点的执行。间隔为0的计时器一旦不再有需要处理的事件就会超时。
QSocketNotifier QNetworkAccessManager QIODevice::readyRead()    这是一种替代使用一个或多个线程(每个线程在慢速网络连接上阻塞读取)的方法。只要可以快速执行响应大量网络数据的计算,这种响应式设计就比线程中的同步等待更好。与线程设计相比,反应式设计更不容易出错,而且节能。在许多情况下也有性能优势。
一般来说,建议只使用安全且经过测试的路径,并避免引入特殊线程概念。
QtConcurrent模块提供了一个简单的接口,可以将工作分配到所有处理器的核心上。线程代码完全隐藏在QtConcurrent框架中,所以您不需要考虑细节。然而,当需要与正在运行的线程通信时,QtConcurrent不能被使用,它不应该被用于处理阻塞操作。

应该使用哪种 Qt 线程技术?
参考:

Qt 线程基础知识
以下各节介绍 QObjects 如何与线程交互,程序如何安全地访问来自多个线程的数据,以及异步执行如何在不阻塞线程的情况下生成结果。

QObject和线程
如上所述,开发人员在从其他线程调用对象的方法时必须始终小心谨慎。线程相关性不会更改这种情况。
Qt文档将几个方法标记为线程安全的。postEvent()是一个值得注意的例子。线程安全的方法可以从不同的线程同时调用。

在通常没有并发访问方法的情况下,在发生并发访问之前,调用其他线程中对象的非线程安全方法可能会工作数千次,从而导致意外的行为。编写测试代码并不能完全确保线程的正确性,但这仍然很重要。在Linux上,Valgrind和Helgrind可以帮助检测线程错误。

保护数据的完整性
在编写多线程应用程序时,必须格外小心,以避免数据损坏。有关如何安全地使用线程的讨论,参阅同步线程。

处理异步执行
获取辅助线程结果的一个方法就是等待线程终止。但是,在许多情况下,阻止等待是不能接受的。阻塞等待的另一种选择是异步的结果交付,使用的是已发布的事件或排队的信号和槽。这将产生一定的开销,因为操作的结果不会出现在下一个源代码行中,而是出现在源文件中其他位置的一个槽中。Qt开发人员习惯于处理这种异步行为,因为它与GUI应用程序中使用的事件驱动编程非常相似。

/********************************************

一、关于线程
线程的用途:单个进程内的并发。

1.1、单核CPU
在单核 CPU 上并行工作是一种错觉。对于进程,这种错觉是通过在很短的时间后中断处理器对一个进程的工作而产生的。然后处理器继续下一个过程。为了在进程之间切换,保存当前程序计数器并加载下一个处理器的程序计数器;对寄存器和某些体系结构和操作系统特定数据进行处理。

正如一个 CPU 可以驱动两个或多个进程一样,也可以让 CPU 在一个进程的两个不同代码段上运行。当一个进程启动时,它总是执行一个代码段,该进程被称为主线程。程序可能会决定启动第二个线程。然后,在一个进程中同时处理两个不同的代码序列。在单核 CPU 上通过重复保存程序计数器和寄存器然后加载下一个线程的程序计数器和寄存器来实现并发。在活动线程之间循环不需要程序的合作。当切换到下一个线程时,一个线程可能处于任何状态。

1.2、多核CPU
CPU 设计的当前趋势是具有多个内核。具有多个线程的程序可以分配给多个内核,从而使事情以真正并发的方式发生。因此,将工作分配给多个线程可以使程序在多核 CPU 上运行得更快。

1.3、GUI 线程和工作线程
Qt GUI 必须在主线程中运行。所有QWidget和几个相关的类,例如 QPixmap,在辅助线程中不起作用。辅助线程通常被称为“工作线程”。

1.4、同时访问数据
每个线程都有自己的堆栈,这意味着每个线程都有自己的调用历史和局部变量。与进程不同,线程共享相同的地址空间。下图显示了线程的构建块如何在内存中定位。非活动线程的程序计数器和寄存器通常保存在内核空间中。每个线程都有一个共享的代码副本和一个单独的堆栈。

如果两个线程具有指向同一个对象的指针,则两个线程可能会同时访问该对象,这可能会破坏对象的完整性。很容易想象当同一个对象的两个方法同时执行时会出现很多问题。

有时需要从不同的线程访问一个对象。例如,当存在于不同线程中的对象需要通信时。由于线程使用相同的地址空间,线程交换数据比进程更容易便捷。数据不必序列化和复制。可以传递指针,但必须严格协调哪个线程接触哪个对象。必须防止对一个对象同时执行操作。

二、使用线程
线程基本上有两种使用场景:

需要使用多核处理器加快处理速度。
需要处理耗时操作但要保持 GUI 线程。
2.1、线程的替代方案
开发人员需要非常小心使用线程。启动线程很容易,但很难确保所有共享数据保持一致。问题通常很难发现,因为它们可能只偶尔出现一次或仅在特定的硬件配置上出现。在使用解决某些问题之前,应该考虑可能的替代方案。

QEventLoop::processEvents():在耗时计算期间重复调用事件循环处理程序可防止 GUI 阻塞。但是,此解决方案不能很好地扩展,因为对 processEvents() 的调用可能发生得太频繁,也可能不够频繁,具体取决于硬件。
QTimer:有时可以使用计时器方便地完成后台处理。
QSocketNotifier、
QNetworkAccessManager、QIODevice::readyRead():(网络操作)这是拥有一个或多个线程的替代方案,每个线程在慢速网络连接上阻塞读取。只要响应一大块网络数据的计算可以快速执行,这种反应式设计比线程中的同步等待更好。响应式设计比线程更不容易出错。在许多情况下,还有性能优势。
QtConcurrent 模块提供了一个简单的接口,用于将工作分配给所有处理器的内核。线程代码完全隐藏在 QtConcurrent 框架中,因此不必关心细节。但是,当需要与正在运行的线程进行通信时,不能使用 QtConcurrent,也不应该使用它来处理阻塞操作。

三、类列表
QAtomicInteger:Qt提供的原子操作
QAtomicPointer:Qt提供的原子操作
QFuture:异步运行结果
QFutureSynchronizer:简化了QFuture的同步
QFutureWatcher:监视QFuture
QMutex:互斥量,使一段代码在一段时间内只能由一个线程访问
QMutexLocker:互斥锁,关联QMutex对象。创建时锁定、销毁时解锁QMutex对象
QReadLocker:读取锁,用作简化 QReadWriteLock 的读取
QReadWriteLock:读写锁
QRecursiveMutex:互斥量,同一线程内可以多次调用 lock()
QRunnable:线程池中的线程要执行的内容
QSemaphore:信号量
QSemaphoreReleaser:确保安全释放信号量
QThread:管控一个线程
QThreadPool:线程池
QThreadStorage:一个模板类,可以为每个线程储存指定类型的数据
QWaitCondition:线程等待条件
QWriteLocker:写入锁,用作简化 QReadWriteLock 的写入
QtConcurrent:Qt并发命名空间
四、Qt中的多线程技术
Qt 提供了许多用于处理线程的类和函数。下面是 Qt 程序员可以用来实现多线程应用程序的四种不同方法。

4.1、QThread:具有可选事件循环的低级 API
QThread 是 Qt 中所有线程控制的基础。 每个 QThread 实例代表并控制一个线程。

QThread 可以直接实例化或子类化。实例化 QThread 提供了一个并行事件循环,允许在辅助线程中调用 QObject 槽函数。子类化 QThread 允许应用程序在启动其事件循环之前初始化新线程,或者在没有事件循环的情况下运行并行代码。

4.2、QThreadPool 和 QRunnable:重用线程
频繁地创建和销毁线程可能会很昂贵。为了减少这种开销,可以将现有线程重用于新任务。QThreadPool 是可重用 QThread 的集合。

要在 QThreadPool 的线程之一中运行代码,请重新实现 QRunnable::run() 并实例化子类 QRunnable。使用 QThreadPool::start() 将 QRunnable 放入 QThreadPool 的运行队列中。当一个线程可用时, QRunnable::run() 中的代码将在该线程中执行。

每个 Qt 应用程序都有一个全局线程池,可以通过 QThreadPool::globalInstance() 访问。 这个全局线程池会根据 CPU 中的内核数自动维护最佳线程数。 但是,可以显式创建和管理单独的 QThreadPool。

4.3、Qt Concurrent:使用高级 API
Qt Concurrent 模块提供了处理一些常见并行计算模式的高级函数:map、filter 和 reduce。与使用 QThread 和 QRunnable 不同,这些函数从不需要使用低级线程原语,例如互斥体或信号量。相反,它们返回一个 QFuture 对象,该对象可用于在函数准备好时检索函数的结果。QFuture 还可用于查询计算进度和暂停/恢复/取消计算。为方便起见,QFutureWatcher 支持通过信号和槽与 QFuture 进行交互。

Qt Concurrent 的映射、过滤和缩减算法会自动在所有可用的处理器内核之间分配计算。

该模块还提供了 QtConcurrent::run() 函数,它可以在另一个线程中运行任何函数。但是,QtConcurrent::run() 仅支持 map、filter 和 reduce 函数可用的功能子集。QFuture 可用于检索函数的返回值并检查线程是否正在运行。但是,对 QtConcurrent::run() 的调用仅使用一个线程,无法暂停/恢复/取消,也无法查询进度。

4.4、WorkerScript:QML 中的线程
WorkerScript QML 类型允许 JavaScript 代码与 GUI 线程并行运行。

每个 WorkerScript 实例都可以附加一个 .js 脚本。当 WorkerScript.sendMessage() 被调用时,脚本将在单独的线程(和单独的 QML 上下文)中运行。当脚本完成运行时,它可以将回复发送回 GUI 线程,该线程将调用 WorkerScript.onMessage() 信号处理程序。

使用 WorkerScript 类似于使用已移动到另一个线程的 QObject。数据通过信号在线程之间传输。

4.5、以上各方法比较
特点

QThread

QRunnable 和 QThreadPool

QtConcurrent::run()

Qt Concurrent

(Map, Filter, Reduce)

WorkerScript

语言

C++

C++

C++

C++

QML

可以指定线程优先级

Yes

Yes

可以运行事件循环

Yes

可以通过信号接收数据更新

Yes(QObject接收信号) 

Yes(WorkerScript 接收信号)

可以使用信号控制

Yes(QThread接收信号)

Yes(QFutureWatcher 接收信号)

可以通过 QFuture 监控

部分

Yes

内置暂停/恢复/取消功能

Yes

4.6、使用示例
线程的生命周期    操作    解决方案
一次性    在另一个线程中运行一个新的线性函数,可在运行期间更新进度    Qt 提供了不同的解决方案:
将该函数放在 QThread::run() 的重新实现中并启动 QThread。 发出信号以更新进度。
将该函数放在 QRunnable::run() 的重新实现中,并将 QRunnable 添加到 QThreadPool。 写入线程安全变量以更新进度。
使用 QtConcurrent::run() 运行该函数。写入线程安全变量以更新进度
一次性    在另一个线程中运行现有函数并获取其返回值    使用 QtConcurrent::run() 运行该函数。当函数返回时,让 QFutureWatcher 发出 finished() 信号,并调用 QFutureWatcher::result() 来获取函数的返回值。
一次性    使用所有可用内核对容器的所有项目执行操作。例如,从图像列表中生成缩略图    使用 Qt Concurrent 的 QtConcurrent::filter() 函数选择容器元素,使用QtConcurrent::map() 函数对每个元素应用操作。要将输出折叠成单个结果,请改用 QtConcurrent::filteredReduced() 和 QtConcurrent::mappedReduced()。
一次性/永久    在纯 QML 应用程序中执行长时间计算,并在结果准备好后更新GUI。    将计算代码放在 .js 脚本中并将其附加到 WorkerScript 实例。调用WorkerScript.sendMessage() 以在新线程中开始计算。 让脚本也调用 sendMessage(),将结果传回 GUI 线程。 在 onMessage 中处理结果并在其中更新 GUI。
永久    有一个对象存在于另一个线程中,该线程可以根据请求执行不同的任务和/或可以接收新数据以进行处理    将 QObject 子类化以创建worker对象。实例化这个worker对象和一个 QThread。将工作线程移动到新线程。通过队列方式连接的信号槽向worker对象发送命令或数据。
永久    在另一个线程中重复执行昂贵的操作,该线程不需要接收任何信号或事件    直接在 QThread::run() 的重新实现中编写无限循环。在没有事件循环的情况下启动线程。让线程发出信号以将数据发送回 GUI 线程。
五、同步线程
虽然线程的目的是让代码并行运行,但有时线程必须停止并等待其他线程。例如,如果两个线程同时尝试写入同一个变量,结果是不确定的。Qt 提供了用于同步线程的低级原语和高级机制。 

5.1、低级同步原语
QMutex 是强制互斥量的基本类。一个线程锁定一个互斥量以获取对共享资源的访问。如果第二个线程在互斥已经被锁定的情况下试图锁定它,第二个线程将被置于睡眠状态,直到第一个线程完成其任务并解锁互斥锁。

QReadWriteLock 与 QMutex 类似,只是它区分了“读”和“写”访问。当一块数据没有被写入时,多个线程同时读取是安全的。QReadWriteLock 允许同时读取共享数据,从而提高并行性。

QSemaphore 是 QMutex 的泛化,它保护一定数量的相同资源。相比之下,QMutex 只保护一个资源。

QWaitCondition 不是通过强制互斥而是通过提供条件变量来同步线程。虽然其他原语使线程等待资源解锁,但 QWaitCondition 使线程等待直到满足特定条件。要允许等待的线程继续进行,请调用wakeOne() 唤醒一个随机选择的线程或wakeAll () 同时唤醒它们。

5.1.1、风险

如果一个线程锁定了一个资源但没有解锁它,应用程序可能会冻结,因为其他线程将永久无法使用该资源。例如,如果抛出异常并强制当前函数返回而不释放其锁,就会发生这种情况。

另一个类似的场景是死锁,例如,假设线程 A 正在等待线程 B 解锁资源,如果线程 B 也在等待线程 A 解锁不同的资源,那么两个线程将永远等待,因此应用程序将冻结。

5.1.2、便利类

QMutexLocker、QReadLocker 、 QWriteLocker 是便利类,它们使 QMutex 和 QReadWriteLock 的使用更容易。它们在构造时锁定资源,并在销毁时自动解锁。它们旨在简化使用 QMutex 和 QReadWriteLock 的代码,从而减少资源被意外永久锁定的可能性。

5.2、高级事件队列 
Qt 的事件系统对于线程间通信非常有用。每个线程可能有自己的事件循环。要调用另一个线程中的槽函数(或任何可调用方法),请将调用放在目标线程的事件循环中。这让目标线程在槽开始运行之前完成其当前任务,而原始线程继续并行运行。

要将调用置于事件循环中,请建立一个Qt::QueuedConnection类型的信号槽连接。每当发出信号时,其参数将被事件系统记录。信号接收者所在的线程将运行该槽。或者,调用QMetaObject::invokeMethod() 不用信号也能达到同样的效果,两种情况都必须使用Qt::QueuedConnection连接,因为直接连接绕过了事件系统,会在当前线程中立即运行该方法。
与使用低级原语不同,使用事件系统进行线程同步时没有死锁的风险。但是,事件系统不强制互斥。如果可调用方法访问共享数据,它们仍然必须用低级原语保护。

话虽如此,Qt 的事件系统,连同隐式共享的数据结构,提供了传统线程锁定的替代方案。如果信号和槽被独占使用,并且线程之间不共享变量,则多线程程序可以完全没有低级原语。

六、可重入和线程安全的概念
6.1、可重入函数和线程安全函数
可重入函数:函数可以同时被多个线程调用,但是每个调用者只能使用自己的数据,而不能使用共享数据。
线程安全函数:函数可以同时被多个线程调用,调用者可以使用共享数据,共享数据的使用是串行的,即一个线程使用时完全占用共享数据,用完了再由第二个线程完全占用使用。
6.2、可重入类和线程安全类
可重入类,它的成员函数可以被多个线程安全地调用,只要每个线程使用这个类的不同的对象。
线程安全类,它的成员函数能够被多线程安全地调用,即使所有的线程都使用该类的同一个实例也没有关系。
因此,线程安全函数总是可重入的,但可重入函数并不总是线程安全的。

七、QObject和线程
QThread 继承了 QObject,它发出信号来指示线程开始或完成执行。QObjects 可以在多个线程中使用,发出调用其他线程中的槽的信号。

7.1、QObject 重入
QObject 是可重入的。它的大多数非 GUI 子类,例如 QTimer、QTcpSocket、QUdpSocket 和 QProcess 也是可重入的,使得可以同时从多个线程使用这些类。注意这些类是设计为从创建和使用在单个线程中。

在一个线程中创建对象并从另一个线程调用其函数不能保证工作。需要注意三个约束:

QObject 的子对象必须始终在创建父对象的线程中创建。
事件驱动的对象只能在单线程中使用,具体来说,这适用于定时器机制和网络模块。如定时器或Socket不能在线程A创建而在线程B中启动。
必须确保在删除 QThread 之前删除在线程中创建的所有对象。
尽管 QObject 是可重入的,但 GUI 类,尤其是 QWidget 及其所有子类是不可重入的。它们只能在主线程中使用。QCoreApplication::exec() 也必须从该线程调用。

通常,不支持在 QApplication 之前创建 QObjects,因为可能导致退出时奇怪的崩溃,具体取决于平台。这意味着也不支持 QObject 的静态实例。结构合理的单线程或多线程应用程序应该使 QApplication 成为第一个创建,最后一个销毁 QObject。

7.2、线程的事件循环
每个线程都可以有自己的事件循环。初始线程使用 QCoreApplication::exec() 启动其事件循环,或者对于单对话框 GUI 应用程序,有时使用 QDialog::exec()。其他线程可以使用 QThread::exec() 启动事件循环。和QCoreApplication一样,QThread提供了一个exit(int)函数和一个quit()函数。

QObject 对象存在于创建它的线程中。该对象的事件由该线程的事件循环调度。使用 QObject::thread() 可以获取QObject 所在的线程。

QObject::moveToThread() 函数更改对象及其子对象的线程(如果对象有父对象,则不能移动对象)。

在拥有该对象的线程以外的线程调用 QObject 上的 delete 是不安全的,除非保证该对象在那一刻不处理事件。使用 QObject :: deleteLater ()则可以安全删除对象,此函数会发布一个 DeferredDelete 事件,该对象的线程的事件循环最终会在处理池事件时删除对象。

可以随时使用线程安全函数 QCoreApplication::postEvent() 手动将事件发布到任何线程中的任何对象。事件将由创建该对象的线程的事件循环自动调度。

所有线程都支持事件过滤器,限制是监控对象必须和被监控对象在同一个线程中。类似的,QCoreApplication::sendEvent()只能用于将事件分派给在调用函数的线程中存活的对象。

7.3、从其他线程访问 QObject 子类
QObject 及其所有子类都不是线程安全的。整个事件传递系统也不是线程安全的。

如果您在不存在于当前线程中的 QObject 子类上调用函数并且该对象可能接收事件,则必须使用互斥锁保护对 QObject 子类内部数据的所有访问,否则,可能会遇到崩溃或其他不希望的情况行为。

与其他对象一样,QThread 对象存在于创建对象的线程中,而不是在调用 QThread::run() 时创建的线程中。在 QThread 子类中提供槽函数通常是不安全的,除非保护带有互斥锁的成员变量。

另一方面,可以安全地从 QThread::run () 实现中发出信号,因为信号发出是线程安全的。

7.4、跨线程的信号和槽
信号和槽

QObject::connect() 本身是线程安全的。

八、Qt 模块中的线程支持
8.1、线程和 SQL 模块
连接只能在创建它的线程内使用。不支持在线程之间移动连接或从不同线程创建查询。

8.2、线程中绘制
QPainter 可以在线程中用于在 QImage、QPrinter 、QPicture 绘画设备上绘画。不支持在 QPixmaps 和 QWidgets 上绘画。

在给定的绘制设备上一次只能有一个线程绘制。

8,3、线程和富文本处理
QTextDocument、QTextCursor 和所有相关的类都是可重入的。

8.4、线程和 SVG 模块
QtSvg 模块中的 QSvgGenerator 和 QSvgRenderer 类是可重入的。

8.5、线程和隐式共享类
Qt 对其许多值类使用称为隐式共享的优化,特别是 QImage 和 QString。隐式共享类可以安全地跨线程复制。它们是完全可重入的。

在很多人的印象中,隐式共享和多线程是不兼容的概念,因为引用计数通常是这样做的,但是 Qt 使用原子引用计数来确保共享数据的完整性,避免引用计数器的潜在损坏。

请注意,原子引用计数不保证线程安全。在线程之间共享隐式共享类的实例时应使用适当的锁定。这与所有可重入类的要求相同。但是,建议使用信号和槽在线程之间传递数据,因为这无需任何显式锁定即可完成。

隐式共享类确实是隐式共享的,即使在多线程应用程序中,也可以安全地使用它们,就好像它们是普通的、非共享的、可重入的基于值的类一样。

/**********************************************************

GUI线程
Qt应用程序exec后就会生成一个线程,这个线程就是主线程,在GUI程序中也称为GUI线程。主线程也是唯一允许创建QApplication或QCoreAppliation对象,比并且可以对创建的对象调用exec()的线程,从而进入事件循环。

在只有主线程即单线程的情况中,每一个事件的发生都需要进入事件循环进行等待,如有在某一步计算量比较大,则会一直占用CPU不放,导致其它操作无法完成,界面陷入冻结状态
所以,对于计算量大的操作,需要放到一个单独的线程进行计算,然后通过信号槽的方式和GUI线程进行通信。

QT多线程的实现方式
1. 重写QThread的run()
实现方法:
新建一个类,继承QThread,重写虚函数run();

  class WorkerThread : public QThread
  {
      Q_OBJECT
      void run() override {
          QString result;
          /* ... here is the expensive or blocking operation ... */
          emit resultReady(result);
      }
  signals:
      void resultReady(const QString &s);
  };
 
  void MyObject::startWorkInAThread()
  {
      WorkerThread *workerThread = new WorkerThread(this);
      connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
      connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
      workerThread->start();
  }

优点
可以通过信号槽与外界通信
缺点
每次新建一个线程都需要继承QThread,实现一个新类,使用不太方便。
要自己进行内存的管理(线程的释放和删除);频繁的创建和释放会给系统带来比较大的开销。
适用场景
QThread适用于那些常驻内存的任务

2. QThread的moveToThread**
实现方法
创建一个集成QObject的类(myObject),new 一个QThread,并调用moveToThread(),将创建和的myObject类移动到子线程中,子线程(myObject)通过发发送信号,利用信号槽机制,与主线程进行通信。

 
  class Worker : public QObject
  {
      Q_OBJECT
 
  public slots:
      void doWork(const QString ¶meter) {
          QString result;
          /* ... here is the expensive or blocking operation ... */
          emit resultReady(result);
      }
 
  signals:
      void resultReady(const QString &result);
  };
 
  class Controller : public QObject
  {
      Q_OBJECT
      QThread workerThread;
  public:
      Controller() {
          Worker *worker = new Worker;
          worker->moveToThread(&workerThread);
          connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
          connect(this, &Controller::operate, worker, &Worker::doWork);
          connect(worker, &Worker::resultReady, this, &Controller::handleResults);
          workerThread.start();
      }
      ~Controller() {
          workerThread.quit();
          workerThread.wait();
      }
  public slots:
      void handleResults(const QString &);
  signals:
      void operate(const QString &);
  };

优点
1、相对重写QThread::run()函数的方法更加灵活:

moveToThread对比传统子类化Qthread更灵活,仅需要把你想要执行的代码放到槽,movetothread这个object到线程,然后拿一个信号连接到这个槽就可以让这个槽函数在线程里执行。可以说,movetothread给我们编写代码提供了新的思路,当然不是说子类化qthread不好,只是你应该知道还有这种方式去调用线程。

轻量级的函数可以用movethread,多个短小精悍能返回快速的线程函数适用 ,无需创建独立线程类,例如你有20个小函数要在线程内做, 全部扔给一个QThread。而我觉得movetothread和子类化QThread的区别不大,更可能是使用习惯引导。又或者你一开始没使用线程,但是后边发觉这些代码还是放线程比较好,如果用子类化QThread的方法重新设计代码,将会有可能让你把这一段推到重来,这个时候,moveThread的好处就来了,你可以把这段代码的从属着movetothread,把代码移到槽函数,用信号触发它就行了。其它的话movetothread它的效果和子类化QThread的效果是一样的,槽就相当于你的run()函数,你往run()里塞什么代码,就可以往槽里塞什么代码,子类化QThread的线程只可以有一个入口就是run(),而movetothread就有很多触发的入口。

3. QRunnalble的run**
Qrunnable是所有可执行对象的基类。我们可以继承Qrunnable,并重写虚函数
实现方法
1、继承QRunnable。和QThread使用一样, 首先需要将你的线程类继承于QRunnable。
2、重写run函数。还是和QThread一样,需要重写run函数,run是一个纯虚函数,必须重写。
3、使用QThreadPool启动线程

class Runnable:public QRunnable
{
       //Q_OBJECT   注意了,Qrunnable不是QObject的子类。
public:
       Runnable();
       ~Runnable();
       void run();
};
 
 
Runnable::Runnable():QRunnable()
{
 
}
 
Runnable::~Runnable()
{
       cout<<"~Runnable()"<<endl;
}
 
void Runnable::run()
{
       cout<<"Runnable::run()thread :"<<QThread::currentThreadId()<<endl;
       cout<<"dosomething ...."<<endl;
}
int main(int argc, char *argv[])
{
       QCoreApplication a(argc, argv);
       cout<<"mainthread :"<<QThread::currentThreadId()<<endl;
       Runnable runObj;
       QThreadPool::globalInstance()->start(&runObj);
       returna.exec();
}

优点:
无需手动释放资源,QThreadPool启动线程执行完成后会自动释放。
缺点:
不能使用信号槽与外界通信。
适用场景:
QRunnable适用于线程任务量比较大,需要频繁创建线程。QRunnable能有效减少内存开销。
和QThread的区别

与外界通信方式不同。由于QThread是继承于QObject的,但QRunnable不是,所以在QThread线程中,可以直接将线程中执行的结果通过信号的方式发到主程序,而QRunnable线程不能用信号槽,只能通过别的方式,等下会介绍。

启动线程方式不同。QThread线程可以直接调用start()函数启动,而QRunnable线程需要借助QThreadPool进行启动。

资源管理不同。QThread线程对象需要手动去管理删除和释放,而QRunnable则会在QThreadPool调用完成后自动释放。

Qt线程之QRunnable的使用详解

4. QtConcurrent的run**
Concurrent是并发的意思,QtConcurrent是一个命名空间,提供了一些高级的 API,使得在编写多线程的时候,无需使用低级线程原语,如读写锁,等待条件或信号。使用QtConcurrent编写的程序会根据可用的处理器内核数自动调整使用的线程数。这意味着今后编写的应用程序将在未来部署在多核系统上时继续扩展。

QtConcurrent::run能够方便快捷的将任务丢到子线程中去执行,无需继承任何类,也不需要重写函数,使用非常简单。详见前面的文章介绍,这里不再赘述。

需要注意的是,由于该线程取自全局线程池QThreadPool,函数不能立马执行,需要等待线程可用时才会运行。
实现方法
1、首先在.pro文件中加上以下内容:QT += concurrent

2、包含头文件#include ,然后就可以使用QtConcurrent了

QFuture fut1 = QtConcurrent::run(func, QString(“Thread 1”)); fut1.waitForFinished();

#include <QtCore/QCoreApplication>
#include <QDebug>
#include <QThread>
#include <QString>
#include <QtConcurrent/QtConcurrentRun>
#include <QTime>
#include<opencv2\opencv.hpp>
#include"XxwImgOp.h"
#ifdef _DEBUG
#pragma comment(lib,".\\XxwImgOpd.lib")
#else
#pragma comment(lib,".\\XxwImgOp.lib")
#endif // _DEBUG
 
using namespace QtConcurrent;
 
XxwImgOp xxwImgOp;
cv::Mat src = cv::imread("1.bmp", 0);
cv::Mat  dst, dst1, dst2;
 
void hello(cv::Mat src)
{
    qDebug() << "-----------" << QTime::currentTime()<<"------------------------"<<QThread::currentThreadId();
    xxwImgOp.fManualThreshold(src, dst, 50, 150);
    qDebug() <<"************" << QTime::currentTime() <<"**********************"<< QThread::currentThreadId();
 
}
 
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QFuture<void> f1 = run(hello,  src);
    QFuture<void> f2 = run(hello, src);
    //阻塞调用,阻塞主线程直到计算完成
    f1.waitForFinished();
    f2.waitForFinished();
 
    //阻塞为End的执行顺序
    qDebug() << "End";
    return a.exec();
}

特点:

//调用外部函数 QFuture f1 =QtConcurrent::run(func,QString(“aaa”));

//调用类成员函数 QFuture f2 =QtConcurrent::run(this,&MainWindow::myFunc,QString(“bbb”));

要为其指定线程池,可以将线程池的指针作为第一个参数传递进去

向该函数传递参数,需要传递的参数,则跟在函数名之后

可以用run函数的返回值funIr来控制线程。
如: funIr.waitForFinished(); 等待线程结束,实现阻塞。
funIr.isFinished() 判断线程是否结束
funIr, isRunning() 判断线程是否在运行
funIr的类型必须和线程函数的返回值类型相同,可以通过
funIr.result() 取出线程函数的返回值

缺点
不能直接用信号和槽函数来操作线程函数,eg : 当线程函数结束时,不会触发任何信号。

多线程间的通信
方法一
将多线程类对象封装为GUI界面类的类成员
然后在子线程定义信号函数,通过信号槽机制,向界面组件emit发射信号,从而实现间接操作.

方法二
使用QApplication::postEvent()实现向界面发送事件,从而能够封装一个自定义类

方法三
使用Invokes()函数来调用界面组件的信号槽

一般使用该函数(用来调用对方的私有信号或槽):

该函数的连接方式默认使用的是Qt::AutoConnection
表示如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
比如,当我们想调用一个obj下的compute(QString, int, double)槽函数时:
则只需要写入:

QMetaObject::invokeMethod(obj, "compute",
                            Q_ARG(QString, "sqrt"),                        
                            Q_ARG(int, 42),
                            Q_ARG(double, 9.7));

注意
在QThread线程中不能直接创建QWidget之类的界面组件.
因为在QT中,所有界面组件相关的操作都必须在主线程中(也就是GUI thread)
所以, QThread线程不能直接操作界面组件.

易犯错误

1、子线程中操作UI

Qt创建的子线程中是不能对UI对象进行任何操作的,即QWidget及其派生类对象,这个是我掉的第一个坑。可能是由于考虑到安全性的问题,所以Qt中子线程不能执行任何关于界面的处理,包括消息框的弹出。正确的操作应该是通过信号槽,将一些参数传递给主线程,让主线程(也就是Controller)去处理。

2、信号的参数问题

元对象系统即是提供了Qt类对象之间的信号槽机制的系统。要使用信号槽机制,类必须继承自QObject类,并在私有声明区域声明Q_OBJECT宏。当一个cpp文件中的类声明带有这个宏,就会有一个叫moc工具的玩意创建另一个以moc开头的cpp源文件(在debug目录下),其中包含了为每一个类生成的元对象代码。
在使用connect函数的时候,我们一般会把最后一个参数忽略掉

我们一般会用到方式是有三种:

* 自动连接(AutoConnection),默认的连接方式。如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;如果发送者与接受者处在不同线程,等同于队列连接。

直接连接(DirectConnection)。当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行。
*
队列连接(QueuedConnection)。当控制权回到接受者所在线程的事件循环时,槽函数被调用。这时候需要将信号的参数塞到信号队列里。槽函数在接受者所在线程执行。

signals:
    //自定义发送的信号
    void myThreadSignal(const int, string, string, string, string);

貌似没什么问题,然而实际运行起来槽函数根本就没有被调用,程序没有崩溃,VS也没报错。在查阅了N多博客和资料中才发现,在线程间进行信号槽连接时,参数不能随便写。
为什么呢?我的后四个参数是标准库中的string类型,这不是元对象系统内置的类型,也不是c++的基本类型,系统无法识别,然后就没有进入信号槽队列中了,自然就会出现问题。解决方法有三种,最简单的就是使用Qt的数据类型了

第二种方法就是往元对象系统里注册这个类型。注意,在qRegisterMetaType函数被调用时,这个类型应该要确保已经被完好地定义了。

qRegisterMetaType<MyClass>("MyClass");

方法三是改变信号槽的连接方式,将默认的队列连接方式改为直接连接方式,这样的话信号的参数直接进入槽函数中被使用,槽函数立刻调用,不会进入信号槽队列中。但这种方式官方认为有风险,不建议使用。

connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::DirectConnection)

还有几点需要注意

一定要用信号槽机制,别想着直接调用,你会发现并没有在子线程中执行。
自定义的类不能指定父对象,因为moveToThread函数会将线程对象指定为自定义的类的父对象,当自定义的类对象已经有了父对象,就会报错。
当一个变量需要在多个线程间进行访问时,最好加上voliate关键字,以免读取到的是旧的值。当然,Qt中提供了线程同步的支持,比如互斥锁之类的玩意,使用这些方式来访问变量会更加安全。
/***************************************

QT中的线程与事件循环理解

1.需要使用多线程管理的例子

  一个进程可以有一个或更多线程同时运行。线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理。Qt 使用QThread 来管理线程。

    QWidget *widget = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout;
    widget->setLayout(layout);
    QLCDNumber *lcdNumber = new QLCDNumber(this);
    layout->addWidget(lcdNumber);
    QPushButton *button = new QPushButton(tr("Start"), this);
    layout->addWidget(button);
    setCentralWidget(widget);

    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, [=]() {
        static int sec = 0;
        lcdNumber->display(QString::number(sec++));
    });

    //WorkerThread *thread = new WorkerThread(this);
    connect(button, &QPushButton::clicked, [=]() {
        timer->start(1);
        for (int i = 0; i < 2000000000; i++);
        timer->stop();
    });

  我们的主界面有一个用于显示时间的 LCD 数字面板还有一个用于启动任务的按钮。程序的目的是用户点击按钮,开始一个非常耗时的运算,程序中我们以一个 2000000000 次的循环来替代这个非常耗时的工作。同时 LCD 开始显示逝去的毫秒数。毫秒数通过一个计时器QTimer进行更新。计算完成后,计时器停止。这是一个很简单的应用,也看不出有任何问题。但是当我们开始运行程序时,问题就来了:点击按钮之后,程序界面直接停止响应,直到循环结束才开始重新更新。

   对于执行需要耗时的操作,需要多线程来进行处理。这是因为 Qt 中所有界面都是在 UI 线程中(也被称为主线程,就是执行了QApplication::exec()的线程),在这个线程中执行耗时的操作(比如那个循环),就会阻塞 UI 线程,从而让界面停止响应。界面停止响应,用户体验自然不好,不过更严重的是,有些窗口管理程序会检测到你的程序已经失去响应,可能会建议用户强制停止程序,这样一来你的程序可能就此终止,任务再也无法完成。

2.最简单有效的方法是,通过在在UI主线程中某个功能进行非常耗时的处理时,如等待一个信号,等,一种间接的方法是,在循环等待的语句中添加QT的下列语句,这样就不会处理主UI卡死的问题,但是,这种解决方法适应面窄,只能用于处理等待中结果的主循环卡死现象。如下列语句:

    connect(button, &QPushButton::clicked, [=]() {
        timer->start(1);
        for (int i = 0; i < 2000000000; i++){
            QCoreApplication::processEvents();
        };
        timer->stop();
    });

  这样主UI线程就不会卡死。但是指标不治本。还要通过线程的方法来控制。

3. 我们增加了一个WorkerThread类。WorkerThread继承自QThread类,重写了其run()函数。我们可以认为,run()函数就是新的线程需要执行的代码。在这里就是要执行这个循环,然后发出计算完成的信号。而在按钮点击的槽函数中,使用QThread::start()函数启动一个线程(注意,这里不是run()函数)。再次运行程序,你会发现现在界面已经不会被阻塞了。另外,我们将WorkerThread::deleteLater()函数与WorkerThread::finished()信号连接起来,当线程完成时,系统可以帮我们清除线程实例。这里的finished()信号是系统发出的

class WorkerThread : public QThread
{
    Q_OBJECT
public:
    WorkerThread(QObject *parent = 0)
        : QThread(parent)
    {
    }
protected:
    void run()
    {
        for (int i = 0; i < 1000000000; i++);
        emit done();
    }
signals:
    void done();
};


    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, [=]() {
        static int sec = 0;
        lcdNumber->display(QString::number(sec++));
    });

    WorkerThread *thread = new WorkerThread(this);
    connect(thread, &WorkerThread::done, timer, &QTimer::stop);
    connect(thread, &WorkerThread::finished, thread, &WorkerThread::deleteLater);
    connect(button, &QPushButton::clicked, [=]() {
        timer->start(1);
        thread->start();
    });

3. 线程理解

  • 可重入的(Reentrant):如果多个线程可以在同一时刻调用一个类的所有函数,并且保证每一次函数调用都引用一个唯一的数据,就称这个类是可重入的(Reentrant means that all the functions in the referenced class can be called simultaneously by multiple threads, provided that each invocation of the functions reference unique data.)。大多数 C++ 类都是可重入的。类似的,一个函数被称为可重入的,如果该函数允许多个线程在同一时刻调用,而每一次的调用都只能使用其独有的数据。全局变量就不是函数独有的数据,而是共享的。换句话说,这意味着类或者函数的使用者必须使用某种额外的机制(比如锁)来控制对对象的实例或共享数据的序列化访问
  • 线程安全(Thread-safe):如果多个线程可以在同一时刻调用一个类的所有函数,即使每一次函数调用都引用一个共享的数据,就说这个类是线程安全的(Threadsafe means that all the functions in the referenced class can be called simultaneously by multiple threads even when each invocation references shared data.)。如果多个线程可以在同一时刻访问函数的共享数据,就称这个函数是线程安全的。

   进一步说,对于一个类,如果不同的实例可以被不同线程同时使用而不受影响,就说这个类是可重入的;如果这个类的所有成员函数都可以被不同线程同时调用而不受影响,即使这些调用针对同一个对象,那么我们就说这个类是线程安全的。由此可以看出,线程安全的语义要强于可重入。

  Qt 是事件驱动的。在 Qt 中,事件由一个普通对象表示(QEvent或其子类)。这是事件与信号的一个很大区别:事件总是由某一种类型的对象表示,针对某一个特殊的对象,而信号则没有这种目标对象。所有QObject的子类都可以通过覆盖QObject::event()函数来控制事件的对象。

 4. 事件与事件队列,事件分发器,事件循环

  事件可以由程序生成,也可以在程序外部生成。

  • QKeyEventQMouseEvent对象表示键盘或鼠标的交互,通常由系统的窗口管理器产生;
  • QTimerEvent事件在定时器超时时发送给一个QObject,定时器事件通常由操作系统发出;
  • QChildEvent在增加或删除子对象时发送给一个QObject,这是由 Qt 应用程序自己发出的。

   需要注意的是,与信号不同,事件并不是一产生就被分发。

  事件产生之后被加入到一个队列中(这里的队列含义同数据结构中的概念,先进先出),该队列即被称为事件队列事件分发器遍历事件队列,如果发现事件队列中有事件,那么就把这个事件发送给它的目标对象。这个循环被称作事件循环。事件循环的伪代码描述大致如下所示: 

while (is_active)
{
    while (!event_queue_is_empty) {
        dispatch_next_event();
    }
    wait_for_more_events();
}

  正如前面所说的,调用QCoreApplication::exec() 函数意味着进入了主循环。我们把事件循环理解为一个无限循环,直到QCoreApplication::exit()或者QCoreApplication::quit()被调用,事件循环才真正退出。

  伪代码里面的while会遍历整个事件队列,发送从队列中找到的事件;wait_for_more_events()函数则会阻塞事件循环,直到又有新的事件产生。我们仔细考虑这段代码,在wait_for_more_events()函数所得到的新的事件都应该是由程序外部产生的。因为所有内部事件都应该在事件队列中处理完毕了。因此,我们说事件循环在wait_for_more_events()函数进入休眠,并且可以被下面几种情况唤醒:

  • 窗口管理器的动作(键盘、鼠标按键按下、与窗口交互等);
  • 套接字动作(网络传来可读的数据,或者是套接字非阻塞写等);
  • 定时器;
  • 由其它线程发出的事件(我们会在后文详细解释这种情况)。

  至于为什么需要事件循环,我们可以简单列出一个清单:

  • 组件的绘制与交互QWidget::paintEvent()会在发出QPaintEvent事件时被调用。该事件可以通过内部QWidget::update()调用或者窗口管理器(例如显示一个隐藏的窗口)发出。所有交互事件(键盘、鼠标)也是类似的:这些事件都要求有一个事件循环才能发出。
  • 定时器:长话短说,它们会在select(2)或其他类似的调用超时时被发出,因此你需要允许 Qt 通过返回事件循环来实现这些调用。
  • 网络:所有低级网络类(QTcpSocketQUdpSocket以及QTcpServer等)都是异步的。当你调用read()函数时,它们仅仅返回已可用的数据;当你调用write()函数时,它们仅仅将写入列入计划列表稍后执行。只有返回事件循环的时候,真正的读写才会执行。注意,这些类也有同步函数(以waitFor开头的函数),但是它们并不推荐使用,就是因为它们会阻塞事件循环。高级的类,例如QNetworkAccessManager则根本不提供同步 API,因此必须要求事件循环。

5, 不要阻塞事件循环

  有了事件循环,你就会想怎样阻塞它。阻塞它的理由可能有很多,例如我就想让QNetworkAccessManager同步执行。在解释为什么永远不要阻塞事件循环之前,我们要了解究竟什么是“阻塞”。假设我们有一个按钮Button,这个按钮在点击时会发出一个信号。这个信号会与一个Worker对象连接,这个Worker对象会执行很耗时的操作。当点击了按钮之后,我们观察从上到下的函数调用堆栈:

main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork()

  我们在main()函数开始事件循环,也就是常见的QApplication::exec()函数。窗口管理器侦测到鼠标点击后,Qt 会发现并将其转换成QMouseEvent事件,发送给组件的event()函数。这一过程是通过QApplication::notify()函数实现的。注意我们的按钮并没有覆盖event()函数,因此其父类的实现将被执行,也就是QWidget::event()函数。这个函数发现这个事件是一个鼠标点击事件,于是调用了对应的事件处理函数,就是Button::mousePressEvent()函数。我们重写了这个函数,发出Button::clicked()信号,而正是这个信号会调用Worker::doWork()槽函数

  在worker努力工作的时候,事件循环在干什么?或许你已经猜到了答案:什么都没做!事件循环发出了鼠标按下的事件,然后等着事件处理函数返回。此时,它一直是阻塞的,直到Worker::doWork()函数结束。注意,我们使用了“阻塞”一词,也就是说,所谓阻塞事件循环,意思是没有事件被派发处理,因为事件分发被阻塞在某个地方运行,不能够继续执行其他的事件分发。

  在事件就此卡住时,组件也不会更新自身(因为QPaintEvent对象还在队列中),也不会有其它什么交互发生(还是同样的原因),定时器也不会超时并且网络交互会越来越慢直到停止。也就是说,前面我们大费周折分析的各种依赖事件循环的活动都会停止。这时候,需要窗口管理器会检测到你的应用程序不再处理任何事件,于是告诉用户你的程序失去响应。这就是为什么我们需要快速地处理事件,并且尽可能快地返回事件循环。

6, 解决阻塞事件循环的方法

  重点来了:我们不可能避免业务逻辑中的耗时操作,那么怎样做才能既可以执行那些耗时的操作,又不会阻塞事件循环呢?

一般会有三种解决方案:

第一,我们将任务移到另外的线程, 多线程处理,新建线程处理该耗时任务;

第二,我们手动强制运行事件循环。想要强制运行事件循环,我们需要在耗时的任务中一遍遍地调用QCoreApplication::processEvents()函数。QCoreApplication::processEvents()函数会发出事件队列中的所有事件,并且立即返回到调用者。仔细想一下,我们在这里所做的,就是模拟了一个事件循环。

第三:使用使用QEventLoop类重新进入新的事件循环,即进行局部事件循环处理。通过调用QEventLoop::exec()函数,我们重新进入新的事件循环,给QEventLoop::quit()槽函数发送信号则退出这个事件循环。

  局部事件循环如下:

QEventLoop eventLoop;
connect(netWorker, &NetWorker::finished,  &eventLoop, &QEventLoop::quit);
QNetworkReply *reply = netWorker->get(url);
replyMap.insert(reply, FetchWeatherInfo);
eventLoop.exec();

  QNetworkReply没有提供阻塞式 API,并且要求有一个事件循环。我们通过一个局部的QEventLoop来达到这一目的:当网络响应完成时,这个局部的事件循环也会退出。即在局部,可以通过局部时间循环,异质处理NetWorker获取get完成内容,如果NetWorker结束,则自动关闭该局部的事件循环。

(1)防止局部事件循环的递归调用问题

  通过“其它的入口”进入事件循环要特别小心:因为它会导致递归调用!现在我们可以看看为什么会导致递归调用了。

  函数中调用了QCoreApplication::processEvents()函数时,用户再次点击按钮,槽函数Worker::doWork()又一次被调用:而在doWork中又进入了另一个局部事件循环。

main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork() // <strong>第一次调用</strong>
QCoreApplication::processEvents() // <strong>手动发出所有事件</strong>
[…]
QWidget::event(QEvent * ) // <strong>用户又点击了一下按钮…</strong>
Button::mousePressEvent(QMouseEvent *)
Button::clicked() // <strong>又发出了信号…</strong>
[…]
Worker::doWork() // <strong>递归进入了槽函数!</strong>

  这种情况也有解决的办法:即在强制执行时间循环是,可以选择性的接受事件类型,如

  我们可以在调用QCoreApplication::processEvents()函数时传入QEventLoop::ExcludeUserInputEvents参数,意思是不要再次派发用户输入事件(这些事件仍旧会保留在事件队列中)。

7. QT中的多线程

  Qt 对线程的支持可以追溯到2000年9月22日发布的 Qt 2.2。在这个版本中,Qt 引入了QThread。不过,当时对线程的支持并不是默认开启的。Qt 4.0 开始,线程成为所有平台的默认开启选项(这意味着如果不需要线程,你可以通过编译选项关闭它,不过这不是我们现在的重点)。现在版本的 Qt 引入了很多类来支持线程。

  (1)QThread是第一个类。它也是 Qt 线程类中最核心的底层类。由于 Qt 的跨平台特性,QThread要隐藏掉所有平台相关的代码。正如前面所说,要使用QThread开始一个线程,我们可以创建它的一个子类,然后覆盖其QThread::run()函数, 然后我们这样使用新建的类来开始一个新的线程:

class Thread : public QThread
{
protected:
    void run()
    {
        /* 线程的相关代码 */
    }
};

Thread *thread = new Thread;

thread->start(); // 使用 start() 开始新的线程

  注意,从 Qt 4.4 开始,QThread就已经不是抽象类了。QThread::run()不再是纯虚函数,而是有了一个默认的实现。这个默认实现其实是简单地调用了QThread::exec()函数,而这个函数,按照我们前面所说的,其实是开始了一个事件循环。而对于最新的QT版本,则更简单,继承Run函数后,只需要start()就可以启动线程的时间循环了。

  (2)QRunnable是我们要介绍的第二个类。这是一个轻量级的抽象类,用于开始一个另外线程的任务。这种任务是运行过后就丢弃的。由于这个类是抽象类,我们需要继承QRunnable,然后重写其纯虚函数QRunnable::run()

  要真正执行一个QRunnable对象,我们需要使用QThreadPool类。顾名思义,这个类用于管理一个线程池。通过调用QThreadPool::start(runnable)函数,我们将一个QRunnable对象放入QThreadPool的执行队列。一旦有线程可用,线程池将会选择一个QRunnable对象,然后在那个线程开始执行。所有 Qt 应用程序都有一个全局线程池,我们可以使用QThreadPool::globalInstance()获得这个全局线程池;与此同时,我们也可以自己创建私有的线程池,并进行手动管理。

  需要注意的是,QRunnable不是一个QObject,因此也就没有内建的与其它组件交互的机制。为了与其它组件进行交互,你必须自己编写低级线程原语,例如使用 mutex 守护来获取结果等。

  (3)QtConcurrent是我们要介绍的最后一个对象。这是一个高级 API,构建于QThreadPool之上,用于处理大多数通用的并行计算模式:map、reduce 以及 filter。它还提供了QtConcurrent::run()函数,用于在另外的线程运行一个函数。注意,QtConcurrent是一个命名空间而不是一个类,因此其中的所有函数都是命名空间内的全局函数。

不同于QThreadQRunnableQtConcurrent不要求我们使用低级同步原语:所有的QtConcurrent都返回一个QFuture对象。这个对象可以用来查询当前的运算状态(也就是任务的进度),可以用来暂停/回复/取消任务,当然也可以用来获得运算结果。注意,并不是所有的QFuture对象都支持暂停或取消的操作。比如,由QtConcurrent::run()返回的QFuture对象不能取消,但是由QtConcurrent::mappedReduced()返回的是可以的。QFutureWatcher类则用来监视QFuture的进度,我们可以用信号槽与QFutureWatcher进行交互(注意,QFuture也没有继承QObject)。

  三个多线程相关的类的对比:

  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值