QObject模型和moveToThread

QObject

  每个线程都以QThread实例表示,并且在内部拥有一个QThreadData来表示线程的基本数据。
事件处理是建立在线程上进行的,每个线程拥有一个待处理事件列表postEventList,保存了待处理的事件QPostEvent(如鼠标、键盘以及用户事件),同时每个线程维护了一个QEventLoop栈,但只有栈顶的QEventLoop对象参与当前事件处理

  包括QThread对象在内,每个QObject对象都可以属于一个线程,但QObject与其父对象须属于同一线程,如某对象有父对象或是widget,则不能更换线程。QThreadData是该对象所属的线程对象,在QObject创建时在其构造函数中被设置。
QThread的Private类是QThreadPrivate,里面包含成员QThreadData *data。这个QThreadData包含了这个线程的一些重要数据,比如下面的几个

1
2
3
4
5
QStack<QEventLoop *> eventLoops;
QPostEventList postEventList; // 当前线程的待处理事件列表
QThread *thread; // 当前线程的线程对象
Qt:: HANDLE threadId; // 实际的线程句柄
QAtomicPointer<QAbstractEventDispatcher > eventDispatcher; // 事件分发器,负责读取和处理数据

  一个线程的事件循环为驻足在该线程中的所有QObjects派发了所有事件,其中包括在这个线程中创建的所有对象,或是移植到这个线程中的对象。一个QObject的线程关联性(thread affinity)是指该对象驻足(live in)在某个线程内。在任何时间都可以通过调用QObject::thread()来查询线程关联性,它适用于在QThread对象构造函数中构建的对象。 QObject对象的事件处理函数始终要在其所关联线程的上下文中执行。

moveToThread概述

函数对QObject子类的对象有以下要求:

  • parent非0的对象不能被移动!
  • QWidget及其派生类对象不能被移动!
  • 该函数必须在对象关联的线程内调用!

moveToThread()有三大任务:

  1. moveToThread_helper函数:生成并通过sendEvent()派发 QEvent::ThreadChange事件,在QObject::event中处理
  2. setThreadData_helper函数:将该对象在当前事件队列中的事件移动到目标线程的事件队列中
  3. 解除在当前线程中的timer注册,在目标线程中重新注册

函数可以改变一个QObject的依附性;它将改变这个对象以及它的孩子们的依附性。因为QObject不是线程安全的,我们必须在对象所驻足的线程中使用此函数;也就是说,你只能将对象从它所驻足的线程中推送到其他线程中,而不能从其他线程中拉回来。

moveToThread源码分析

moveToThread的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void QObject::moveToThread(QThread *targetThread)
{
if (d->parent != 0) {
qWarning("QObject::moveToThread: Cannot move objects with a parent");
return;
}
if (d->isWidget) {
qWarning("QObject::moveToThread: Widgets cannot be moved to a new thread");
return;
}
......
else if (d->threadData != currentData) {
qWarning("QObject::moveToThread: Current thread (%p) is not the object's thread (%p)./n"
}
//省略,获得当前线程和目标线程的数据
//对当前对象和子对象调用sendEvent派发QEvent::ThreadChange,
d->moveToThread_helper();
//将当前线程的事件队列转移到目标线程的事件队列
d_func()->setThreadData_helper(currentData, targetData);

前面三个if确定了函数的三条使用要求。

moveToThread_helper的源码:

1
2
3
4
5
6
7
Q_Q(QObject);
QEvent e(QEvent::ThreadChange);
QCoreApplication::sendEvent(q, &e);
for (int i = 0; i < children.size(); ++i) {
QObject *child = children.at(i);
child->d_func()->moveToThread_helper();
}

setThreadData_helper的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 将当前线程的事件队列转移到目标线程的事件队列
int eventsMoved = 0;
for (int i = 0; i < currentData->postEventList.size(); ++i) {
const QPostEvent &pe = currentData->postEventList.at(i);
if (!pe.event)
continue;
if (pe.receiver == q) {
// move this post event to the targetList
targetData->postEventList.addEvent(pe);
const_cast<QPostEvent &>(pe).event = 0;
++eventsMoved;
}
}
if (eventsMoved > 0 && targetData->eventDispatcher.load()) {
targetData->canWait = false;
targetData->eventDispatcher.load()->wakeUp();
}
. . . . . .
for (int i = 0; i < children.size(); ++i) {
QObject *child = children.at(i);
child->d_func()->setThreadData_helper(currentData, targetData);
}

参考:QObject模型


using关键字

引入命名空间,一个 using 声明一次只引入一个命名空间,这个不用讲了

使用class有时可能会遇到这种情况:基类中的函数func有多个重载版本,但是派生类只对版本1进行了重定义。 结果派生类 隐藏 了基类中所有版本,通过派生类只能访问重定义的版本1,不能再访问基本的版本2,3,4,则派生类要么重定义所有重载版本,或者一个也不重定义。 如果基类中重载版本太多,在派生类中全重定义一遍就太麻烦了。

这就是using关键字适用的场合,可以在派生类中为重载成员名称提供 using 声明,使派生类使用的全是基类的函数版本。在此基础上,再重定义需要的那些函数,其他版本仍是基类的版本。

说的太抽象了,看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base
{
public:
Base() {}
virtual ~Base() {}

virtual void func(int i) { std::cout << "func in base " <<i<< endl;}
virtual void func(float i) { std::cout << "func in base " <<i<< endl;}
};

class Derived : public Base
{
public:
Derived() {}
// using Base::func;
virtual void func(int i) { std::cout << "func in derived " <<i*5<< endl;}
};

Derived d;
d.func(12);
d.func(float(12.20));

不加using时,运行结果是:

1
2
func in derived 60
func in derived 60

也就是对于d.func(float(12.20));,派生类仍然调用了自己的版本,但是我们实际上没有定义float参数的虚函数,这并不是我们想要的。

using后,只有参数是int的重载版本是派生类的,派生类没有定义虚函数的情况,还是调用基类的。运行结果是:

1
2
func in derived 60
func in base 12.2

隐藏对虚函数和非虚函数都是适用的,因为Effective C++说不要在派生类重新定义基类的非虚函数,所以这里我定义成了虚函数,由于多个虚函数对sizeof的计算相当于一个,所以不会扩大占用内存,可以仍按照Effective C++说的声明为虚函数。

using 用做 alias

1
2
3
using Type = std::vector<int>;

Type v={1,2,3,4,5,6};

再探默认构造函数

今天使用派生类时又发现了一个问题,基类和派生类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base
{
public:
Base(int a)
{ }
};

class Derived : public Base
{
public:
Derived()
{ }
};

还没编译,Creator就已经报错了
2
编译时Derived类会报错: error: no matching function for call to ‘Base::Base()’

这个问题的实质还是默认构造函数。我们都知道,如果一个类没有构造函数,编译器会为我们自动创建一个默认构造函数,这个函数没有参数,什么都没做。但是,当我们实现一个构造函数之后,编译器就不会创建,因此Base没有默认构造函数。

默认构造函数就是在调用时不需要explicitly传入实参的构造函数

上面我定义的Base(int a)是构造函数,而不是默认构造函数。 C++中,最多有一个默认构造函数,刻意是编译器生成,也可以是我们自己定义的。我们自己定义的默认构造函数只能是两种:

  1. 无参 2. 有参数,但参数都有默认值

比如下面两种都是默认构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base
{
public:
Base()
{ }
};
// 或者有默认值的有参构造函数
class Base
{
public:
Base(int a=0)
{ }
};

但是不要把这两种构造函数都放入类里,因为最多只能有一个默认构造函数。不实例化不报错,一旦实例化就会报错: 指定了多个默认构造函数,对重载函数调用不明确

派生类构造函数对基类初始化

再回到上面的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base{
public:
Base(int value){}
};

class Derived : public Base
{
public:
Derived()
{ }
private:
int m_value;
};

现在加入派生类,以上的代码会报错: error: constructor for ‘Derived’ must explicitly initialize the base class ‘Base’ which does not have a default constructor

如果基类没有默认构造函数,那么编译器也不会为派生类隐式地定义默认构造函数, 就会出现上面的错误。对这种错误,有两种修改方法:

  1. 对于有参的构造函数,参数赋默认值。也就是改成默认构造函数:把Base(int a)改为Base(int a=0)

  2. 最常用,也是Qt中所用的:在派生类的构造函数后,用列表初始化的方式调用基类构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Derived : public Base
    {
    public:
    Derived():
    Base(m_value)
    { }
    private:
    int m_value;
    };

上面的问题,可以总结如下:

  • 基类没有显式声明构造函数或者有一个无参数的构造函数,派生类构造函数可以不用对基类初始化,即忽略基类的构造函数
  • 基类的构造函数全是有参数的,派生类必须至少实现一个基类的构造函数,例如Qt中常见的:
1
2
3
4
5
explicit MainWindow(QWidget* parent=0);
// QMainWindow构造函数都有参数
MainWindow::Mainwindow(QWidget* parent):
QMainWindow(parent),
ui(new Ui::MainWindow)

这种方式解决的问题是:使用派生类创建一个对象后,怎样初始化从基类中继承过来的数据成员?(基类的构造函数是不能被继承的)

这种代码的具体格式:

1
2
3
4
派生类::派生类构造函数(总参数列表):基类构造函数(参数列表)
{
派生类中的数据成员初始化;
}

注意:如果没有基类和派生的关系,就不能用这种初始化格式,否则报错。

C++11中的default关键字

之前说了,如果我们显式声明构造函数,编译器就不会生成默认构造函数。 但是有时候,我们反而需要一个默认构造函数,比如下面的情况:

1
2
3
4
5
6
7
8
9
10
11
12
class C
{
public:
C(int f)
{
qDebug()<<"construct" ;
}
int a;
long b;
};

C obj;

很显然会报错,因为不存在默认构造函数了。我们可以加一个构造函数:C()=default;,这样编译就通过了

但是以下用法都会报错:

1
2
Base(double value=0)=default;
Base(float value)=default;

這是因为= default只能被加在 沒有默认参数 的special member function后面,Special member function包含: Default constructor, Destructor, Copy constructor, Copy assignment, Move constructor, Move assignment


Qt中多线程的使用(二)

线程池

当线程的任务量比较大时,频繁创建和销毁线程会有很大的内存开销,此时使用QThread的方法就不合适,应该使用线程池QThreadPool。QThread适用于常驻内存的任务,QThreadPool适用于不常驻内存,任务量比较大的情况。

QRunnable 是一个非常轻量的抽象类,它的主体是纯虚函数 QRunnable::run(),我们需要继承它并实现这个函数。使用时需要将其子类的实例放进 QThreadPool 的执行队列,线程池默认会在运行后自动删除这个实例。每一个Qt程序都有一个全局的线程池,我们可以直接使用它,这就不需要手动创建和管理线程池,调用QThreadPool::globalInstance()得到,可在多个类中共同使用一个线程池。它默认最多创建 8 个线程,如果想改变最大线程数则调用 setMaxThreadCount() 进行修改,调用 activeThreadCount() 查看线程池中当前活跃的线程数。

Qt并不是推荐使用其全局线程池,何况实际的项目当中我们通常并不希望仅仅使用一个全局的线程池,而是在需要线程池的工程中都构建和维护自己一个小小的线程池。用QThreadPool pool;的方式建立一个局部线程池,并由当前类维护,可保证此线程池仅供当前类应用。

常规使用

定一个任务类例如叫 Task,继承 QRunnable 并实现虚函数 run(),Task 的对象作为 QThreadPool::start() 的参数就可以了,线程池会自动的在线程中调用 Task 的 run() 函数,异步执行。线程池中的 QRunnable 对象太多时并不会立即为每一个 QRunnable 对象创建一个线程,而是让它们排队执行,同时最多有 maxThreadCount() 个线程并行执行。

1
2
3
4
5
6
void clear()	//清除所有当前排队但未开始运行的任务
int expiryTimeout() const //线程长时间未使用将会自动退出节约资源,此函数返回等待时间
void setExpiryTimeout(int expiryTimeout) //设置线程回收的等待时间
int maxThreadCount() const //线程池可维护的最大线程数量
setAutoDelete //用来标识是否在运行结束后自动由线程池释放空间
bool waitForDone(int msecs=-1) //等待所有线程运行结束并退出,参数为等待时间-1表示一直等待到最后一个线程退出

代码实现一个QRunnable子类,构造函数参数是其ID,run()里输出ID和休眠1到3秒,析构函数里输出ID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Task : public QRunnable
{
public:
Task(int id);
void run() Q_DECL_OVERRIDE;
~Task();
private:
int m_id;
};

Task::Task(int id):
m_id(id)
{}

void Task::run()
{
qDebug().noquote() << QString("Start thread %1 at %2").arg(m_id).arg(QDateTime::currentDateTime().toString("mm:ss.z"));
QThread::msleep(1000+qrand()%2000);
}

Task::~Task()
{
qDebug().noquote() << QString("deconstruct Task with ID %1").arg(m_id);
}

main函数中的代码:

1
2
3
4
5
6
7
8
    QThreadPool pool;
pool.setMaxThreadCount(3);
for(int i=0;i<15;i++)
{
Task *t = new Task(i);
// QThreadPool::globalInstance()->start(t); //使用全局线程池
pool.start(t); // 提交任务给线程池,在线程池中执行
}

在main函数中创建一个本地线程池,最大线程数为3,一次创建了15个线程,线程池不是一个一个运行线程,而是让线程进入队列,批量运行,每次3个。运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Start thread 0 at 31:05.66
Start thread 2 at 31:05.66
Start thread 1 at 31:05.66
deconstruct Task with ID 2
deconstruct Task with ID 0
Start thread 3 at 31:06.703
deconstruct Task with ID 1
Start thread 4 at 31:06.704
Start thread 5 at 31:06.704
deconstruct Task with ID 3
Start thread 6 at 31:08.173
deconstruct Task with ID 5
Start thread 7 at 31:08.173
deconstruct Task with ID 4
Start thread 8 at 31:08.173
deconstruct Task with ID 6
Start thread 9 at 31:09.507
deconstruct Task with ID 8
Start thread 10 at 31:09.508
deconstruct Task with ID 7
Start thread 11 at 31:09.508
deconstruct Task with ID 9
Start thread 12 at 31:11.009
deconstruct Task with ID 10
Start thread 13 at 31:11.009
deconstruct Task with ID 11
Start thread 14 at 31:11.01
deconstruct Task with ID 14
deconstruct Task with ID 13
deconstruct Task with ID 12

程序当中QRunnable是以指针的形式创建的,是QThreadPool在运行完线程后自动释放,官方文档有一句:QThreadPool takes ownership and deletes QRunnable object automatically,这也是Qt半自动内存回收机制的一方面。

SIGNAL/SLOT 实现线程池与外界通信

QRunnable 不是 QObject的子类,无法使用信号与槽的机制与外界通信的手段。实现与外界通信有两种方法:

1
2
1. 子类采用QObejct 和 QRunnable多重继承,而且QObject要放在前面,再用信号与槽的机制。
2. 使用`QMetaObject::invokeMethod`。

也就是说,线程通信手段还是那两种。

现在,用线程池实现前一篇的读大文件的程序,思路还是一样的,修改Task类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Task : public QObject,public QRunnable
{
Q_OBJECT
public:
Task(int id,QWidget* m_w);
void run() Q_DECL_OVERRIDE;
~Task();
signals:
void toLine(QString line);
private:
int m_id;
QWidget* m_w; //为调用主线程的槽函数做准备
};

void Task::run()
{
QFile* file = new QFile("E:\qtgui.index");
file->open(QIODevice::ReadOnly);
QTextStream *stream = new QTextStream(file);
qDebug()<<"do work's thread:"<<QThread::currentThread();
while(!stream->atEnd())
{
QString line = stream->readLine();
emit toLine(line);
QThread::msleep(15);
}

主线程的部分程序:
1
2
3
4
5
6
// QThreadPool pool;
pool.setMaxThreadCount(1);
Task *t = new Task(1,this);
QThreadPool::globalInstance()->start(t);
// pool.start(t);
connect(t,SIGNAL(toLine(QString)),this,SLOT(appendText(QString)));

QMetaObject::invokeMethod 实现线程池与外界通信

修改上面的程序: 将emit toLine(line)改为QMetaObject::invokeMethod(m_w,"appendText",Qt::AutoConnection,Q_ARG(QString,line));,主线程中将connect语句去掉即可。

上面的两个程序必须用全局线程池启动线程,如果是本地线程池则无效,还是先运行次线程,阻塞主线程。

QThread和QThreadPool适用的场合

1.耗时的一次计算,I/O或者初始化:
不建议使用QThread,建议使用:QRunnable和QThreadPool结合使用或者直接用QtConcurrent::run

2.周期性的耗时计算或I/O:
使用继承QObject和moveToThread函数的方式,封装到类中,然后后台线程启动驻留,然后信号槽传送计算参数和接收计算的数据。

3.程序线程分模块,例如网络和数据库单独在一个线程或者分别的线程设计:
使用继承QObject和moveToThread函数的方式,封装到类中,然后后台线程启动驻留,然后信号槽传送操作命令和取回结果。(注QWidget的UI操作只能在主线程的)。

4.多个任务执行者,分别的执行,一个调度者:
每个封装一个类,然后执行者和调度者分线程,数个或者单个执行者一个线程,通过信号槽与调度者交互,用moveToThread方式分线程。

5.程序中引用其他库,其他库且有独立的事件循环:
用继承QThread的方式,在run中启用其他库的事件循环,需要与主线程交互的部分采用自定义事件和QCoreApplication::postEvent方式通讯。

6.当不得不重复调用一个方法(比如每秒),许多人会引入多线程,在run中调用sleep(1),其实这是可以用QTimer的timeout信号取代。

参考:多线程使用QTimer
QThreadPool线程池与QRunnable


Qt中多线程的使用(一)

我们要实现的是读取大文件qtgui.index的内容加入文本框中。
预览
很容易想到的方法:

1
2
3
4
5
6
7
8
9
QFile* file = new QFile("E:\qtgui.index");
file->open(QIODevice::ReadOnly);
QTextStream *stream = new QTextStream(file);

while(!stream->atEnd())
{
QString line = stream->readLine();
ui->textEdit->append(line);
}

结果运行后发现程序失去响应。因为读取大文件要很长时间,事件循环一直等待函数返回,这样导致阻塞事件循环。结果,GUI线程所有的绘制和交互都被阻塞在事件队列中,无法执行重绘等事件,整个程序就失去响应了。

解决阻塞一般有两种方法:

手动强制事件循环

在任务中不断调用QCoreApplication::processEvents()手动强制事件循环,它会在处理完队列中所有事件后返回。但是如果两次函数调用的间隔时间不够短,用户仍能明显感觉到程序卡顿。所以在while循环最后加一行QApplication::processEvents();即可。

多线程处理。

Qt提供了三种方式:QThread、QRunnable / QThreadPool、QtConcurrent。其中最常用的是 QThread。

对于本例,使用QThread又有三种方法:信号与槽实现线程间通信、元对象系统实现线程间通信、分离线程与任务。前两种也是跨线程调用函数的方法。

使用QThread

信号与槽实现线程间通信

这是线程间通信比较常用的方法。代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ReadThread : public QThread
{
Q_OBJECT
public:
ReadThread(QObject* obj);
signals:
void toLine(QString line);
protected:
void run() Q_DECL_OVERRIDE;
private:
QFile* file;
QObject* m_obj;
};

ReadThread::ReadThread(QObject* obj):
m_obj(obj)
{
file = new QFile("E:\qtgui.index");
}

void ReadThread::run()
{
file->open(QIODevice::ReadOnly);
QTextStream *stream = new QTextStream(file);
while(1)
{
while(!stream->atEnd())
{
QString line = stream->readLine();
emit toLine(line);
QThread::msleep(15);
}
}
}

需要把读取任务放到run()里,构造函数要传入GUI类的指针

在GUI线程的信号与槽机制这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ReadThread* thread = new ReadThread(this);
thread->start();
connect(thread,SIGNAL(toLine(QString)),this,SLOT(appendText(QString)) );
connect(thread,SIGNAL(finished()),this,SLOT(FinishThread()) );
}

void MainWindow::appendText(QString lineTemp)
{
ui->textEdit->append(lineTemp);
}

其中appendText是MainWindow的槽函数,Q_ARG的两个形参分别为槽函数的形参类型和实参。

在使用invokeMethod方法后,使用了QThread的静态函数msleep,因为读取的文件太大,每读取一行就要更新GUI,太耗资源,会导致GUI忙不过来,读一行后稍微休息一下,否则也会阻塞GUI。

QThread的子类一般不定义槽函数,这是不安全的,可能造成主线程和子线程同时访问它,除非使用mutex保护。 但可以定义signal,而且可以在run函数中发射, 因为信号发射是线程安全的。 #### 元对象系统实现线程间通信 将ReadThread类中的信号去掉,再把run()中的emit所在行换成:
1
2
QMetaObject::invokeMethod(m_obj,"appendText",Qt::AutoConnection,
Q_ARG(QString,line) );
在构造函数中直接运行线程:
1
2
ReadThread* thread = new ReadThread(this);
thread->start();
`QMetaObject::invokeMethod()`中的第三个参数是信号与槽的连接方式,如果目标进程与当前线程相同则用`Qt::DirectConnection`;不同则用`Qt::QueuedConnection`,想对象所在线程发送事件,进入目标线程的事件循环;如果是`Qt::AutoConnection`,则会根据线程的情况自动判断。 显然这里可以用后两者。 这一机制依赖Qt内省机制,所以只有信号、槽、带`Q_INVOKABLE`关键字的成员函数才能使用此方法,本例的appendText为槽函数。 本例的函数形参类型是QString,但如果所调函数的参数类型不是内建数据类型、不属于 QVariant,会报错,即该类型的参数无法进入信号队列。这时需要我们在类的声明之后调用宏`Q_DECLARE_METATYPE(MyClass)`,当然前提是该类提供了公有的构造函数、拷贝构造函数和析构函数,并且要在跨线程通信前使用函数 `qRegisterMetaType("MyClass")`来注册这个类型。 我们知道QWidget及其子类都是不可重入的,也就是**GUI类只能在GUI线程使用**,本例中如果在子线程直接调用`appendText`,可能也能得到正确结果,但这种做法并不正确。 另外我们无法在类外调用private成员函数。本例中我们可以**在子线程使用MainWindow的私有方法**,只要把`appendText`改为带`Q_INVOKABLE`修饰的私有成员函数即可。 ### moveToThread分离线程与任务 一个QObject的线程依附性(thread affinity)是指该对象驻足(live in)在某个线程内。在任何时间都可以通过调用QObject::thread()来查询线程依附性,它适用于在QThread对象构造函数中构建的对象。 一个线程的事件循环为驻足在该线程中的所有QObjects派发了所有事件,其中**包括在这个线程中创建的所有对象,或是移植到这个线程中的对象**。一个QThread的局部事件循环可以通过调用QThread::exec() 来开启(它包含在run()方法的内部) 将计算任务和线程管理分离,即在一个 QObject 中处理任务,并使用 QObject::moveToThread 改变QObject的依附性。因为QObject不是线程安全的,我们必须在对象所驻足的线程(一般是GUI线程)中使用此函数;也就是说,你只能将对象从它所驻足的线程中推送到其他线程中,而不能从其他线程中拉回来。 QThread**所依附的线程**,就是执行` QThread * t=new QThread()` 的线程,一般就是GUI线程。QThread**管理的线程**,就是 run 启动的线程,也就是子线程。线程ID只能在run()函数中调用`QThread::currentThreadId()`查看。 此外,Qt要求一个QObject的孩子必须与它们的父对象驻足在同一个线程中。这意味着:不能使用QObject::moveToThread()作用于有父对象的对象; 千万不要在一个线程中创建对象的同时把QThread对象自己作为它的父对象。比如这种做法是错的:
1
2
3
4
class MyThread : public QThread {
void run() {
QObject* obj = new QObject(this);
} };
然后在GUI线程的构造函数里创建MyThread对象,运行线程后会报错: QObject: Cannot create children for a parent that is in a different thread. (Parent is MyThread(0x2d07e70), parent's thread is QThread(0x2cea418) 也就是说MyThread对象在GUI线程,而obj在子线程。 moveToThread底层是依赖Qt事件循环实现的(QCoreApplication::postEvent),所以使用moveToThread必须是在开启Qt事件循环的程序中,就是main函数中调用QCoreApplication::exec的程序。 自定义QObject的子类MyObj,注意不能是QWidget的子类,因为它不可重入:
1
2
3
4
5
6
7
8
9
10
class MyObj : public QObject
{
Q_OBJECT
public:
MyObj();
signals:
void toLine(QString line);
private slots:
void doWork();
};
源文件的代码,主要是槽函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
void MyObj::doWork()
{
QFile* file = new QFile("E:\qtgui.index");
file->open(QIODevice::ReadOnly);
QTextStream *stream = new QTextStream(file);
qDebug()<<"do work's thread:"<<QThread::currentThread();
while(!stream->atEnd())
{
QString line = stream->readLine();
emit toLine(line);
QThread::msleep(15);
}
}
槽函数实现读取任务,方法与2.1.1类似,关键是信号`toLine`和休眠函数。 mainwindow.cpp的代码,私有变量t和obj在头文件里声明:
1
2
3
4
5
6
7
8
9
10
11
t = new QThread();      //QThread
obj = new MyObj();
obj->moveToThread(t);
qDebug()<<"main thread:"<<QThread::currentThread();

connect(t,SIGNAL(started()), obj, SLOT(doWork()));
connect(obj,SIGNAL(toLine(QString)),this,SLOT(appendText(QString) ) );
connect(t,SIGNAL(finished()), obj, SLOT(deleteLater()) );

//connect(this,SIGNAL(closeMe()), t, SLOT(terminate()) );
t->start();
第一个connect是启动线程t后,执行任务处理的槽函数;第二个connect是obj执行中发出信号后,文本框添加文本;第三个connect是等线程t结束时,删除obj指针;启动线程t后就可以读取文件并刷新GUI了。 停止子线程的方法最好是给while循环添加布尔量做控制,以及`t->quit(); t->wait();`。 注意: 发出信号toLine的obj和this不是同一个线程。

代码中的默认connect类型是Qt::AutoConnection,如果在一个线程就是Qt::DirectConnection,不在一个线程就是Qt::QueuedConnection;
如果是Qt::DirectConnection,相当于直接调用槽函数,但是当信号发出的线程和槽的对象不在同一个线程的时候,槽函数是在发出的信号中执行的。所以appendText在子线程。
如果是Qt::QueuedConnection,线程安全,内部通过postEvent实现的。不是实时调用的,槽函数永远在槽函数对象所在的线程中执行。所以appendText在GUI线程

QueuedConnection的线程情况:
QueuedConnection

DirectConnection 的线程情况:
DirectConnection

AutoConnection 的线程情况:
AutoConnection

同步调用:发出信号后,当前线程等待槽函数执行完毕后才继续执行。
异步调用:发出信号后,立即执行剩下逻辑,不关心槽函数什么时候执行。

总结:

参考:Qt信号槽的一些事
Qt线程使用杂谈


构造函数的成员初始化列表 (一)

但是C++11规定,类的声明中可以给非静态成员变量赋初值:

1
2
3
4
5
class Base
{
public:
int m = 3;
};

类中的const成员进行初始化必须用这种方式

1
2
3
4
5
6
7
8
9
10
11
12
class Father {
public:
explicit Father(int father):
f(father),
m(10)
{

}
private:
const int f;
const int m;
};

如果不用这种方式初始化,会编译报错:error: C2789:必须初始化常量限定类型的对象

引用成员变量必须在列表中初始化

比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base
{
public:
Base();
explicit Base(int a);
private:
int m=4; // 类体内赋值,只适用于C++11
const int abc;
int& f;
};

Base::Base(int a)
:abc(10), // 常量
f(m) //引用
{
}

如果没有在构造函数成员列表初始化,会报错error: uninitialized reference member in ‘int&’ ‘const int’ [-fpermissive]
也就是上面的1和2的情况

类中的引用必须用列表初始化的方式赋值,不能在类体内或构造函数中用=赋值,否则会报错:operator= is private

两个类没有继承关系,但是类B的对象是类A成员变量,而B的构造函数都是有参数的,而且参数没有默认值,此时必须在类A构造函数中对B初始化

举例:Father类跟上面一样,增加类MyClass如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass
{
public:
explicit MyClass()
:father(s)
{

}
private:
Father father; // 没有默认构造行为的成员
int s;
};
class Father
{
public:
Father(int a){cout<<"Father construct"<<endl;}
};

MyClass构造函数前,先运行Father的构造函数,所以需要对其初始化,否则报错。这里实际上用到了类的隐式转换。
只要Father类有一个函数是无参数的,那就不需要在MyClass类中对father显式初始化,让编译器自动完成即可。其实就是初始化Father构造函数的参数。

我们有更好的优化方法:把father改成指针类型,实现对此规则的优化,这也是Qt中的常见方法
。 因为定义在heap上的类的指针是不会运行构造函数的,类的对象要运行构造函数,然后用成员初始化列表的方法对指针初始化:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass
{
public:
explicit MyClass()
:father(new Father(s))
{

}
private:
Father* father;
int s;
};

类似的,Qt中的ui指针所指对象的初始化可以放到构造函数里:

1
2
3
4
5
6
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
ui = new Ui::MainWindow;
ui->setupUi(this);
}

构造函数中初始化的顺序

初始化的顺序与列表中的参数顺序无关,而是由类中定义的顺序决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Base::Base(int a)
{
std::cout<<"constructor Base:"<<a<<endl;
}

class Derive : public Base
{
public:
Derive();
Base a;
Base b;
};
Derive::Derive():
b(8),
a(4)
{
}

Derive* d = new Derive();

运行结果是先a后b:
1
2
constructor Base:4
constructor Base:8

与初始化时a和b的顺序无关,只跟类Derive中的声明顺序有关。

静态变量不能用构造函数成员列表初始化

静态变量不属于某个对象,而是被类共有,假如构造函数能初始化,会造成变量值不同。编译时报错:error: C2438 无法通过构造函数初始化静态类数据