QThreadPool线程池的原理与使用

一、为什么需要用线程池

现在所有的高性能服务器程序,几乎都会使用到线程池技术,从而更好且有效的榨干服务器性能。

1、开多少个线程可以达到性能最佳

不知道,你有没有这个疑问?这是一种常见的线程使用方式:

class MyThread: public QThread
{
public:
    virtual void run() override
    
{
        // ...
    }
};
MyThread th;
th.start();

线程的创建和销毁是有性能开销的,当我们有少量业务需要处理时,我们可以放到线程中完成,甚至可以多开几个线程并行处理。

那么,问题来了,如果需要海量的数据处理,难道我们无休止的开线程下去吗?

首先,你要明白CPU的性能是有限的,每个线程好比一个处理时间片,多个线程之间切换处理,CPU线程上下文来回切换,这个也是需要消耗时间的。

所以,物极必反,当线程数量到达一个点后,可能消耗在线程切换的时间,会大于实际线程处理业务的时间,这个可以想象的到。

那么很容易明白:线程数并不是越多越好,而是某个范围或者某个经验值。

一般来讲,我们可以认为,最佳性能线程数==CPU逻辑核心数量,比如CPU是4核8线程,那么开8个线程可以达到性能最佳。

一般电脑是开启超线程的,也就是4核可以模拟出8个逻辑核,故称4核8线程。

QThreadPool线程池默认最大线程数,也是CPU逻辑Core的数量。

严格意义来讲,最佳线程数还与处理业务类型有关,如业务属于IO密集型、CPU密集型,根据经验推断:

  • IO密集型,频繁读取磁盘上的数据,或者需要通过网络远程调用接口。线程数经验值是:2N,其中N代表CPU逻辑Core数;

  • CPU密集型,非常复杂的调用,循环次数很多,或者递归调用层次很深等。线程数经验值是:N + 1,其中N代表CPU逻辑Core数。

但是对于线程数的深入讨论研究,不在本文范围内。

2、线程池的原理

通过上一节,我们知道了,最佳性能线程数可以认为等于CPU逻辑核心数量N。

所以我们设计程序,为了得到更好的性能,需要实现如下的需求:

  • 限制创建最大线程数量<=N;

  • 尽可能复用线程,避免频繁创建和销毁线程资源,降低无谓消耗;

  • 线程在空闲时,应该休息,避免占用CPU资源;

  • 线程在有业务需要处理时,需要激活;

  • 当业务来了,这N个线程如何分配;

  • ……

上述问题,感觉很麻烦,对吧。别担心,QThreadPool线程池就是干这些的。

线程池,属于对象池,对象池都是为了复用,以避免频繁申请和释放对象所造成的性能损失。

线程池创建好后,池内默认一个线程也没有,当通过相关函数加入任务后,线程池根据任务数量会自动创建线程,任务会合理分配到各个线程上执行,但是线程总数量不会超过设定的最大值。

若任务处理完毕,则池内所有线程进入挂起状态,不占用CPU时间片,待任务再次到来,便会激活部分或全部线程,处理任务。

若任务过多,当前没有空闲的线程,则新增任务会被放置到缓存队列中,等待线程空闲后,再进行处理,这样,每个任务与线程可以有一个合理的分配,相当于实现了业务处理的负载均衡。故而可以以最好的性能来处理业务。

这就是线程池,存在的重要意义。

在设计高性能程序时,离不开线程池的加持。

小结:

线程池严格限制线程数量,并对线程对象创建和释放进行了封装,实现了线程对象的最大复用。对任务与线程进行了合理分配。从限制线程数、线程复用、任务分配三方面减少无谓消耗,提升了软件业务处理性能。

二、QThreadPool线程池的使用

QThreadPool的方法很少,封装的足够完备,使用也是很简单的。

1、举例说明QThreadPool的使用

语言说的再多,都很晦涩,下面看一个例子main.cpp。

#include <QCoreApplication>
#include <QThreadPool>
#include <QDebug>

class Task1 : public QRunnable
{
public:
    Task1()
    { }
    virtual ~Task1() override
    {
        qDebug() << "~Task1()";
    }

    virtual void run() override
    
{
        qDebug() << "do Task1 work:" << QThread::currentThreadId();
    }
};

class Task2 : public QRunnable
{
public:
    Task2()
    { }
    virtual ~Task2() override
    {
        qDebug() << "~Task2()";
    }

    virtual void run() override
    
{
        qDebug() << "do Task2 work:" << QThread::currentThreadId();
    }
};

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

    Task1* task1 = new Task1();
    Task2* task2 = new Task2();

    QThreadPool threadPool;
    threadPool.start(task1);
    threadPool.start(task2);
    threadPool.waitForDone();

    return a.exec();
}

从QRunnable派生出2个任务类Task1、Task2表示2个任务,重写基类run()实现各任务执行的操作。

将2个任务对象通过QThreadPool的start方法传入线程池,表示将任务加入到线程池中,待线程进行处理,线程会调用2个任务的run()。

waitForDone()表示等待,直到所有任务处理完毕,才返回。

运行结果:

QThreadPool线程池的原理与使用
在这里插入图片描述

可以看到2个任务的run()分别在不同的线程中被调用,并行完成处理,处理完后,线程池默认删除这2个任务。

以上就是QThreadPool的基本使用。

2、QThreadPool其他相关方法

下面看看该类还有哪些方法,QThreadPool相关方法声明如下:

class Q_CORE_EXPORT QThreadPool : public QObject
{
public:
    static QThreadPool *globalInstance();

    void start(QRunnable *runnable, int priority = 0);
    bool tryStart(QRunnable *runnable);

    int expiryTimeout() const;
    void setExpiryTimeout(int expiryTimeout);

    int maxThreadCount() const;
    void setMaxThreadCount(int maxThreadCount);

    int activeThreadCount() const;

    void setStackSize(uint stackSize);
    uint stackSize() const;

    void reserveThread();
    void releaseThread();

    bool waitForDone(int msecs = -1);

    void clear();
    bool tryTake(QRunnable *runnable);
};


方法 功能
globalInstance() 返回程序默认的全局内存池实例
start(QRunnable *runnable, int priority = 0) 预定一个线程用于执行QRunnable接口,当预定的线程数量超出线程池的最大线程数后,QRunnable接口将会进入队列,等有空闲线程后,再执行
tryStart(QRunnable *runnable) 试图预定一个线程来运行runnable,如果在调用时没有空闲线程,则此函数不执行任何操作并返回false。否则,使用一个可用线程立即运行runnable,该函数返回true。
expiryTimeout() 超过此时间未使用的线程被认为已经过期并将退出,默认为30s。
setExpiryTimeout(int expiryTimeout) 设置线程过期时间,超过此时间未使用的线程将退出。建议在创建线程池之后,但在调用start()之前设置expiryTimeout。
maxThreadCount() 返回线程池维护的最大线程数量
setMaxThreadCount(int maxThreadCount) 设置线程池维护的最大线程数量
activeThreadCount() 返回线程池中激活线程的数量
setStackSize(uint stackSize) 设置线程池中线程的堆栈大小
stackSize() 返回线程池中线程的堆栈大小
reserveThread() 保留一个线程,不考虑activeThreadCount()和maxThreadCount()。一旦你完成了线程,调用releaseThread()来允许它被重用。注意:这个函数总是会增加活动线程的数量。这意味着,通过使用这个函数,activeThreadCount()可以返回一个大于maxThreadCount()的值。
releaseThread() 释放以前通过调用reserveThread()预约的线程。如果不先预约一个线程,调用这个函数会临时增加maxThreadCount()。当线程进入休眠等待时,能够允许其他线程继续。要记得在完成等待时调用reserveThread(),以便线程池可以正确控制activeThreadCount()。
waitForDone(int msecs = -1) 等待,直到所有任务处理完毕,才返回。
clear() 从队列中删除尚未启动的任务。runnable为true的任务会被删除。
tryTake(QRunnable *runnable)                           如果runnable任务还没开始运行,那么从队列中删除此runable任务,此时函数返回true;如果runnable任务已经运行,返回false。只能用来删除runnable->autoDelete() == false的runnable任务,否则可能会删错任务。


QThreadPool线程池的原理与使用



本篇文章来源于微信公众号: 超哥学编程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/10828.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!