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