QT从入门到入土(五(2))——多线程(QtConcurrent::run())和线程池

引言

在前面对Qt多线程(QThread)做了详细的分析:QT从入门到入土(五(1))——多线程(QThread) - 唯有自己强大 - 博客园 (cnblogs.com)

但是最近在做项目时候,要将一个函数单独运行在另一个线程中,并且这个函数是私有函数,用到的数据也是私有数据,不能通过其他类访问,这样怎么办?如何在Qt中的当前类创建线程?


一, 最简单的多线程QtConcurrent::run()

  • 其函数原型如下:
QFuture<T> QtConcurrent::run(Function function, ...)
QFuture<T> QtConcurrent::run(QThreadPool *pool, Function function, ...)

参数function需要外部函数:(或者lambda函数),后面也可跟外部函数的参数。

extern void func(QString str);

QtConcurrent :: run()也接受指向成员函数的指针。第一个参数必须是一个const引用或一个指向该类实例的指针。const成员函数一般传递 常量引用 (const reference),而非常量成员函数一般传递 指针 (pointer)

  • 在VS环境中需要引用: #include"QtConcurrent/qtconcurrentrun.h"

简单的说,QtConcurrent::run()函数会在一个单独的线程中执行,并且该线程取自全局QThreadPool,该函数的返回值通过QFuture API提供。
请注意:该函数可能不会立即运行; 函数只有在线程可用时才会运行。通过QtConcurrent::run()返回的QFuture不支持取消、暂停,返回的QFuture只能用于查询函数的运行/完成状态和返回值。

  • 实例:实现多线程耗时操作(调用成员函数)

在主程序threadtest.h中声明成员函数(并添加引用)

#include <QtWidgets/QMainWindow>
#include "ui_threadtest.h"
#include"workThread.h"
#include"qthread.h"
#include"QtConcurrent/qtconcurrentrun.h"//QtConcurrent引用
#pragma execution_character_set("utf-8")
class Threadtest : public QMainWindow
{
    Q_OBJECT

public:
    Threadtest(QWidget *parent = Q_NULLPTR);  
private:
    Ui::ThreadtestClass ui;
    void work();//成员函数
};

在主程序threadtest.cpp中调用成员函数

#include "threadtest.h"
#include"qdebug.h"

Threadtest::Threadtest(QWidget* parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);
    connect(ui.btn_start, &QPushButton::clicked, [=]()
        {
//将成员函数放入参数中(成员函数需要引用指针) QtConcurrent::run(
this, &Threadtest::work); }); } void Threadtest::work() { qDebug() << "子线程运行:" << QThread::currentThreadId(); QThread::sleep(5); qDebug() << "子线程结束:" << QThread::currentThreadId(); }

点击按钮即可触发子线程运行。

  • 实例:实现多线程耗时操作(调用外部函数,带参数)

在主程序threadtest.h中声明外部函数

#include <QtWidgets/QMainWindow>
#include "ui_threadtest.h"
#include"workThread.h"
#include"qthread.h"
#include"QtConcurrent/qtconcurrentrun.h"//QtConcurrent引用
#pragma execution_character_set("utf-8")
class Threadtest : public QMainWindow
{
    Q_OBJECT

public:
    Threadtest(QWidget *parent = Q_NULLPTR);  
private:
    Ui::ThreadtestClass ui;
  
};
extern void func(QString str);//外部函数

在主程序threadtest.cpp中调用外部函数

#include "threadtest.h"
#include"qdebug.h"

Threadtest::Threadtest(QWidget* parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);
    connect(ui.btn_start, &QPushButton::clicked, [=]()
        {
            QtConcurrent::run(func,QString("extern"));
        });
}
 void func(QString str)
{
    qDebug() << "子线程运行:" << QThread::currentThreadId()<< str;
    QThread::sleep(5);
    qDebug() << "子线程结束:" << QThread::currentThreadId()<< str;
}

 二,线程池

2.1 线程池原理

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

 

 2.2 QRunnable

在 Qt 中使用线程池需要先创建任务,添加到线程池中的每一个任务都需要是一个 QRunnable 类型,因此在程序中需要创建子类继承 QRunnable 这个类,然后重写 run() 方法,在这个函数中编写要在线程池中执行的任务,并将这个子类对象传递给线程池,这样任务就可以被线程池中的某个工作的线程处理掉了。

// 在子类中必须要重写的函数, 里边是任务的处理流程
[pure virtual] void QRunnable::run();

// 参数设置为 true: 这个任务对象在线程池中的线程中处理完毕, 这个任务对象就会自动销毁
// 参数设置为 false: 这个任务对象在线程池中的线程中处理完毕, 对象需要程序猿手动销毁
void QRunnable::setAutoDelete(bool autoDelete);
// 获取当前任务对象的析构方式,返回true->自动析构, 返回false->手动析构
bool QRunnable::autoDelete() const;

例如:创建一个要添加到线程池中的任务类,处理方式如下

class MyWork : public QObject, public QRunnable
{
    Q_OBJECT
public:
    explicit MyWork(QObject *parent = nullptr)
    {
        // 任务执行完毕,该对象自动销毁
        setAutoDelete(true);
    }
    ~MyWork();

    void run() override{}
}

在上面的示例中 MyWork 类是一个多重继承,如果需要在这个任务中使用 Qt 的信号槽机制进行数据的传递就必须继承 QObject 这个类,如果不使用信号槽传递数据就可以不继承了,只继承 QRunnable 即可。

2.3 QThreadPool

Qt 中的 QThreadPool 类管理了一组 QThreads, 里边还维护了一个任务队列。QThreadPool 管理和回收各个 QThread 对象,以帮助减少使用线程的程序中的线程创建成本。每个Qt应用程序都有一个全局 QThreadPool 对象,可以通过调用 globalInstance() 来访问它。也可以单独创建一个 QThreadPool 对象使用。

线程池常用的 API 函数如下:

// 获取和设置线程中的最大线程个数
int maxThreadCount() const;
void setMaxThreadCount(int maxThreadCount);

// 给线程池添加任务, 任务是一个 QRunnable 类型的对象
// 如果线程池中没有空闲的线程了, 任务会放到任务队列中, 等待线程处理
void QThreadPool::start(QRunnable * runnable, int priority = 0);
// 如果线程池中没有空闲的线程了, 直接返回值, 任务添加失败, 任务不会添加到任务队列中
bool QThreadPool::tryStart(QRunnable * runnable);

// 线程池中被激活的线程的个数(正在工作的线程个数)
int QThreadPool::activeThreadCount() const;

// 尝试性的将某一个任务从线程池的任务队列中删除, 如果任务已经开始执行就无法删除了
bool QThreadPool::tryTake(QRunnable *runnable);
// 将线程池中的任务队列里边没有开始处理的所有任务删除, 如果已经开始处理了就无法通过该函数删除了
void QThreadPool::clear();

// 在每个Qt应用程序中都有一个全局的线程池对象, 通过这个函数直接访问这个对象
static QThreadPool * QThreadPool::globalInstance();

一般情况下,我们不需要在 Qt 程序中创建线程池对象,直接使用 Qt 为每个应用程序提供的线程池全局对象即可。得到线程池对象之后,调用 start() 方法就可以将一个任务添加到线程池中,这个任务就可以被线程池内部的线程池处理掉了,使用线程池比自己创建线程的这种多种多线程方式更加简单和易于维护。

具体的使用方式如下:

  • mywork.h
class MyWork :public QRunnable
{
    Q_OBJECT
public:
    explicit MyWork();
    ~MyWork();

    void run() override;
}
  • mywork.cpp
MyWork::MyWork() : QRunnable()
{
    // 任务执行完毕,该对象自动销毁
    setAutoDelete(true);
}
void MyWork::run()
{
    // 业务处理代码
    ......
}
  • mainwindow.cpp
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 线程池初始化,设置最大线程池数
    QThreadPool::globalInstance()->setMaxThreadCount(4);
    // 添加任务
    MyWork* task = new MyWork;
    QThreadPool::globalInstance()->start(task);    
}

由此看见,线程池的操作和QThread的run重写类似。使用线程池可以最大限度的利用线程,减少资源的浪费

比如我们需要同时处理三个事件:生成随机数,冒泡排序,快速排序(先生成随机数,然后再对其同时进行冒泡排序和快速排序)

用QThread的run重写的话,需要开启三个线程。

但是用线程池的话,它可以根据事件的长短,(将生成随机数和冒泡排序放在同一个线程去处理)只需要开启两个线程即可。

 

posted @ 2021-08-05 16:42  唯有自己强大  阅读(4931)  评论(2编辑  收藏  举报