Qt中的线程池

来源:互联网 发布:淘宝九块九在哪里 编辑:程序博客网 时间:2024/05/19 07:09

总结一下最近使用qt多线程和线程池遇到的一些问题。

文章1:

简述

QRunnable 是所有 runnable 对象的基类,而 QThreadPool 类用于管理 QThreads 集合。

QRunnable 类是一个接口,用于表示一个任务或要执行的代码,需要重新实现 run() 函数。

QThreadPool 管理和循环使用单独的 QThread 对象,以帮助程序减少创建线程的成本。每个 Qt 应用程序都有一个全局 QThreadPool 对象,可以通过调用 globalInstance() 访问。



详细描述

QThreadPool 支持多次执行相同的 QRunnable,通过调用 QThreadPool::tryStart(this) 从 run() 函数内。如果启用了 autoDelete,当最后一个线程退出 run() 函数,QRunnable 将被删除。多次调用 QThreadPool::start() 使用相同的 QRunnable,当启用 autoDelete 时会创建一个竞争条件,不推荐使用。

一定时间未使用线程将会到期,默认到期超时是 30000 毫秒(30秒)。可以使用 setExpiryTimeout() 来改变,设定一个负值,则会禁用到期机制。

调用 maxThreadCount() 查询使用线程的最大数量,如果需要,可以使用 setMaxThreadCount() 进行更改。默认情况下,maxThreadCount() 是 QThread::idealThreadCount()。activeThreadCount() 函数返回当前正在工作线程的数量。

注意: QThread::idealThreadCount() 提供了计算程序运行所在平台上支持的辅助线程的最佳数量 - 考虑到操作系统、处理器的数量和机器拥有的处理核的数量。对于只有一个处理器、一个处理核的机器,该函数或许会返回 1。对于拥有多个处理器和处理核的机器,返回值则会相应增大,这个数字不一定会与需要处理的文件个数完全匹配,因此需要将任务划分,这样的话,每个辅助线程(假设使用的辅助线程大于 1)都能得到一个和需要处理的文件数量相等的数值(当然,用文件数量目来划分任务或许不是在所有情况下都是最好的方法,例如:在一个数量为 20 的文件列表中,前 10 个文件很大,后 10 个 文件很小)。

reserveThread() 函数储备一个线程用于外部使用。当线程完成后,使用 releaseThread(),以便它可以被重新使用。从本质上讲,这些函数暂时增加或减少活跃线程的数量,并且当实现耗时的操作时对 QThreadPool 是不可见的,这比较有用。

注意: QThreadPool 是一个管理线程的低级类,高级替代品可以用 Qt Concurrent 模块。

基本使用

要使用 QThreadPool 的一个线程,子类化 QRunnable 并实现 run() 虚函数。然后创建一个对象,并把它传递给 QThreadPool::start() - 这会把可运行对象的拥有权赋给 Qt 的全局线程池,并可以让它开始运行。

class HelloWorldTask : public QRunnable{    void run() {        qDebug() << "Hello world from thread " << QThread::currentThread();    }}HelloWorldTask *hello = new HelloWorldTask();// QThreadPool取得所有权,并自动删除 helloQThreadPool::globalInstance()->start(hello);

默认情况下,当可运行对象结束时,线程池会自动将其删除,这也正是我们想要的效果。在某些情况下,如果必须由我们自己负责删除可运行的对象时,可以通过调用 QRunnable::setAutoDelete(false) 来阻止自动删除的发生。

自定义信号/槽

打开 QRunnable 所在头文件,会发现它并不继承自 QObject,也就是说,根本无法使用 QObject 的特性,例如:信号/槽、事件等。

为了便于使用,我们可以继承 QObject:

class HelloWorldTask : public QObject, public QRunnable{    Q_OBJECT// 自定义信号signals:    void finished();public:     void run() {         qDebug() << "Hello Thread : " << QThread::currentThreadId();         emit finished();     }};

使用时,连接信号槽即可:

class MainWindow : public QMainWindow{    Q_OBJECTpublic:    explicit MainWindow(QWidget *parent = 0)        : QMainWindow(parent)    {        qDebug() << "Main Thread : " << QThread::currentThreadId();        // ...        HelloWorldTask *hello = new HelloWorldTask();        connect(hello, SIGNAL(finished()), this, SLOT(onFinished()));        QThreadPool::globalInstance()->start(hello);        // ...    }protected:    void closeEvent(QCloseEvent *event) {        if (QThreadPool::globalInstance()->activeThreadCount())            QThreadPool::globalInstance()->waitForDone();        event->accept();    }private slots:    void onFinished() {        qDebug() << "SLOT Thread : " << QThread::currentThreadId();    }};

为了获得安全清楚,对于多线程应用程序来说,最好在终止程序之前,停止所有辅助线程。我们已经通过 closeEvent() 做到了这一点,可以确保在允许终止动作之前让任何活动的线程先结束掉。

顺便再介绍下所属线程,使用 qDebug 将各自的线程 ID 进行调试输出。结果如下:

Main Thread : 0xb308 
Hello Thread : 0xb33c 
SLOT Thread : 0xb308

显然,槽函数所在线程与主线程相同。

如果想要槽函数在次线程中执行,只需改变信号槽的连接方式:

connect(hello, SIGNAL(finished()), this, SLOT(onFinished()), Qt::DirectConnection);

这时,就得到了想要的结果啦:

Main Thread : 0xb030 
Hello Thread : 0xacf8 
SLOT Thread : 0xacf8

事件的传递、数据的交互方式很多,这里只介绍了信号槽。当然还可以使用自定义事件,然后通过 调用 QApplication::sendEvent() 或 QApplication::postEvent() 发送;或者使用 QMetaObject::invokeMethod() 方式均可,这里就不再赘述了。

文章2


第一部分:QT线程池的构建与使用

    网上关于QT线程池QThreadPool的文章很多,而且大都千篇一律,基本上都是参考QT的帮助文档介绍QT全局线程池的用法。这样就往往会使人产生误解,QT是不是推荐大家使用其全局线程池,而不推荐使用自定义构造的线程池? 实际情况并不是这样的。而且在实际的项目当中我们通常并不希望仅仅使用一个全局的线程池,而是在需要线程池的工程中都构建和维护自己一个小小的线程池(我们知道一个良好架构的项目通常是由多个工程组成的)。综上,我们来分析以下两个问题:

    (1)    非全局的线程池如果构建与使用呢?

[cpp] view plain copy
  1. #include <QObject>  
  2. #include <QRunnable>  
  3. #include <QThread>  
  4. #include <QThreadPool>  
  5. #include <QDebug>  
  6. class HelloWorldTask : public QRunnable  
  7. {  
  8.      // 线程执行任务:每间隔1s打印出线程的信息  
  9.     void run()  
  10.     {  
  11.         for (int nCount = 0; nCount < 5; nCount++)  
  12.         {  
  13.         qDebug() << QThread::currentThread();  
  14.         QThread::msleep(1000);  
  15.         }  
  16.     }  
  17. };  
  18. int main(int argc, char *argv[])  
  19. {  
  20.     QCoreApplication a(argc, argv);  
  21.   
  22.     QThreadPool   threadpool;              // 构建一个本地的线程池  
  23.     threadpool.setMaxThreadCount(3);         // 线程池中最大的线程数  
  24.         for (int nNum = 0; nNum < 100; nNum++)  
  25.     {  
  26.         HelloWorldTask   *task;    // 循环构建可在线程池中运行的任务  
  27.         threadpool.start(task);      //线程池分配一个线程运行该任务  
  28.         QThread::msleep(1000);  
  29.     }  
  30.     return a.exec();  
  31. }  

    上述程序,构建了一个线程最大数量为3的本地线程池。每间隔1s的时间创建一个线程任务并置入到线程池的任务队列中(QT内部机制实现该队列,我们只需要调用QThreadPool的start函数置入即可)。每个线程任务的持续时间为5s。

    (2)    程序当中QRunnable是以指针的形式创建的,该指针是需要程序员去释放,还是QThreadPool在运行完线程后自动释放?

    解答:在上述例子当中,我们创建的QRunnable类型的指针 QRunnable *task 是不需要我们手动去回收内存的,QThreadPool在结束该任务的执行后会将对该内存进行清空。

    上述解答并不是凭空猜测,一方面根据是QT文档中的一句话:

    QThreadPool takes ownership and deletes 'hello'automatically

     用直白的话说就是:QThreadPool会占有这个指针的句柄并在运行结束后释放指针所占的内存。

    另一方面,我们也通过改进上面的例子进行验证。

[cpp] view plain copy
  1. #include <QObject>  
  2. #include <QRunnable>  
  3. #include <QThread>  
  4. #include <QThreadPool>  
  5. #include <QDebug>  
  6. class HelloWorldTask : public QRunnable  
  7. {  
  8.      // 线程执行任务:每间隔1s打印出线程的信息  
  9.     void run()  
  10.     {  
  11.             int         m_dataMem[256*1000];                  // 占约 1MB内存空间   
  12.         for (int nCount = 0; nCount < 5; nCount++)  
  13.         {  
  14.         qDebug() << QThread::currentThread();  
  15.         QThread::msleep(1000);  
  16.         }  
  17.     }  
  18. };  
  19. int main(int argc, char *argv[])  
  20. {  
  21.     QCoreApplication a(argc, argv);  
  22.   
  23.     QThreadPool   threadpool;   
  24.     threadpool.setMaxThreadCount(1);   
  25.         for (int nNum = 0; nNum < 100; nNum++)  
  26.     {  
  27.         HelloWorldTask   *task;   
  28.         threadpool.start(task);   
  29.         QThread::msleep(1000);  
  30.     }  
  31.     return a.exec();  
  32. }  

     在程序运行过程中,我们观察发现程序的进程一直仅占用约1MB的内存空间。如果在main函数中所创建的100个HelloWorldTask 指针对象没有被QThreadPool释放的话,随着程序的运行该程序所占内存空间应该逐步攀升到约100MB。然而实际情况是,该程序最高仅占用1MB的内存空间。

    综上两个方面可以得出以下结论:QRunnable创建的对象QThreadPool在执行完该对象后会帮助我们来清空内存,不需要我们手动回收内存。


第二部分:QThread 与QRunnable + QThreadPool适用的应用场景

    QThread是QT的线程类,通过继承QThread然后重写run函数即可实现一个线程类。QThreadPool+ QRunnable配合构建线程池的方法也可以实现线程。我们通过以下问题对上述两种构建线程的方法进行分析和说明。

    (1)既然QThread这么简单我们为什么还要使用QThreadPool + QRunnable创建线程池的方法来使用线程机制呢?

主要原因:当线程任务量非常大的时候,如果频繁的创建和释放QThread会带来比较大的内存开销,而线程池则可以有效避免该问题,相关的基础支持可以自行百度线程池的优点。

    (2)QThread与 QThreadPool + QRunnable分别适用于哪种应用场景?

     QThread适用于那些常驻内存的任务。而且QThread可以通过信号/槽的方式与外界进行通信。而QRunnable则适用于那些不常驻内存,任务数量比较多的情况。

 

第三部分:QRunnable 如何与外界进行通信

       方法1:QRunnable并不继承自QObject类,因此无法使用信号/槽的方式与外界进行通信。我们就必须的使用其他方法,这里给大家介绍的是使用:QMetaObject::invokeMethod()函数。

       方法2:使用多重继承的方法,任务子类同时继承自QRunnable和QObject。




原创粉丝点击