QT多线程的四种实现

前言

        在QT环境下进行桌面应用程序开发的时候,应用程序在某些情况下需要处理比较复杂的逻辑, 如果只有一个线程去处理,易导致窗口卡顿。这种情况下就需要使用多线程,其中一个线程处理窗口事件,其他线程进行各自相关的逻辑操作,用户体验和程序的执行效率都显著提高。

        在qt中使用了多线程,有些事项是需要额外注意的:

        默认的线程在Qt中称之为窗口线程,也叫主线程,负责窗口事件处理或者窗口控件数据的更新;子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,, 如果操作了程序就会崩溃;主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制;

一、Qt线程基础

1.1  Qt提供的线程类

线程类说明
QAtomicInt提供了Integer上与平台无关的Qtomic运算
QAtomicPointer提供了指针上Atomic运算的模板函数
QFuture显示异步运算结果的类
QFutureSynchronizerQFuture类简化同步而提供的类
QFutureWatcher使用信号和槽,允许QFuture监听
QMutex访问类之间的同步
QMutecLocker简化Lock和Unlock Mutex的类
QReadWriteLock控制读写操作的类
QReadLocker为了读访问而提供的
QWriteLocker为了写访问而提供的
QRunnable正在运行的所有对象的父类,且定义了虚函数run()
QSemaphore一般的Count互斥体类
QThread提供与平台无关的线程功能的类
QThreadPool管理线程的类
QThreadStorage提供每个线程存储区域的类
QWaitCondition确认线程间同步的类的状态值

1.2  Qt的QThread类同步机制

        为了实现线程同步,Qt提供了QMutex、QReadWriteLock、QSemaphore和QWaitCondition类。主线程等待与其他线程的中断时,必须进行同步。例如:两个线程同时访问共享变量,那么可能得不到预想的结果。因此,两个线程访问共享变量时,必须进行同步。

  1. 一个线程访问指定的共享变量时,为了禁止其他线程访问,QMutex提供了类似锁定装置的功能。互斥体激活状态下,线程不能同时访问共享变量,必须在先访问的线程完成访问后,其他线程才可以继续访问。
  2. 一个线程访问互斥体锁定的共享变量期间,如果其他线程也访问此共享变量,那么该线程将会一直处于休眠状态,直到正在访问的线程结束访问。这称为线程安全。
  3. QReadWriteLock和QMutex的功能相同,区别在于,QReadWriteLock对数据的访问分为读访问和写访问。很多线程频繁访问共享变量时,与QMetex相对,使用QReadWriteLock更合适。
  4. QSemaphore拥有和QMutex一样的同步功能,可以管理多个按数字识别的资源。QMutex只能管理一个资源,但如果使用QSemaphore,则可以管理多个按号码识别的资源。
  5. 条件符合时,QWaitCondition允许唤醒线程。例如,多个线程中某个线程被阻塞时,通过QWaitCondition提供的函数wakeOne()和wakeAll()可以唤醒该线程。

1.3  可重入性与线程安全

  • 可重入性:两个以上线程并行访问时,即使不按照调用顺序重叠运行代码,也必须保证                     结果;
  • 线程安全:线程并行运行的情况下,虽然保证可以使程序正常运行,但访问静态空间或                   共享(堆等内存对象)对象时,要使用互斥体等机制保证结果

        一个线程安全的函数不一定是可重入的;一个可重入的函数缺也不一定是线程安全的!

        可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

        编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个线程调用此函数时,很有可能使有关全局变量变为不可知状态。

        满足下列条件的函数多数是不可重入的:

  • 函数体内使用了静态的数据结构和全局变量,若必须访问全局变量,利用互斥信号量来保护全局变量;
  • 函数体内调用了malloc()或者free()函数;
  • 函数体内调用了标准I/O函数。

        常见的不可重入函数有:

  • printf ——–引用全局变量stdout
  • malloc ——–全局内存分配表
  • free ——–全局内存分配表

        也就是说:本质上,可重入性与C++类或者没有全局静态变量的函数相似,由于只能访问自身所有的数据变量区域,所以即使有两个以上线程访问,也可以保证安全性。

1.4 处理QThread的信号和槽的类型

Qt提供了可以决定信号与槽类型的枚举类,以在线程环境中适当处理事物。

常量说明
Qt::AutoConnection0如果其他线程中发生信号,则会插入队列,像QueuedConnection一样,否则如DirectConnection一样,直接连接到槽。发送信号时决定Connection类型。
Qt::DirectConnection1发生信号事件后,槽立即响应
Qt::QueuedConnection2返回收到的线程事件循环时,发生槽事件。槽在收到的线程中运行
Qt::BlockingQueuedConnection3与QueuedConnection一样,返回槽时,线程被阻塞。建立在事件发生处使用该类型

1.5  QThread类

  • QThread类可以不受平台影响而实现线程。QThread提供在程序中可以控制和管理线程的多种成员函数和信号/槽。通过QThread类的成员函数start()启动线程。
  • QThread通过信号函数started()和finished()通知开始和结束,并查看线程状态;可以使用isFinished()和isRunning()来查询线程的状态;使用函数exit()和quit()可以结束线程。
  • 如果使用多线程,有时需要等到所有线程终止。此时,使用函数wait()即可。线程中,使用成员函数sleep()、msleep()和usleep()可以暂停秒、毫秒及微秒单位的线程。
  • 一般情况下,wait()和sleep()函数应该不需要,因为Qt是一个事件驱动型框架。考虑监听finished()信号来取代wait(),使用QTimer来取代sleep()。
  • 静态函数currentThreadId()和currentThread()返回标识当前正在执行的线程。前者返回该线程平台特定的ID,后者返回一个线程指针。
  • 要设置线程的名称,可以在启动线程之前调用setObjectName()。如果不调用setObjectName(),线程的名称将是线程对象的运行时类型(QThread子类的类名)。

1.6  QThread类提供的常用API

1.6.1 常用共同成员函数
// QThread 类常用 API
// 构造函数
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判断线程中的任务是不是处理完毕了
bool QThread::isFinished() const;
// 判断子线程是不是在执行任务
bool QThread::isRunning() const;

// Qt中的线程可以设置优先级
// 得到当前线程的优先级
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);
// ***********************************************************************
常量	                        值	优先级
QThread::IdlePriority	        0	没有其它线程运行时才调度
QThread::LowestPriority	        1	比LowPriority调度频率低
QThread::LowPriority	        2	比NormalPriority调度频率低
QThread::NormalPriority	        3	操作系统的默认优先级
QThread::HighPriority	        4	比NormalPriority调度频繁
QThread::HighestPriority	    5	比HighPriority调度频繁
QThread::TimeCriticalPriority	6	尽可能频繁的调度
QThread::InheritPriority	    7	使用和创建线程同样的优先级. 这是默认值
// ***********************************************************************
// 退出线程, 停止底层的事件循环
// 退出线程的工作函数
void QThread::exit(int returnCode = 0);
// 调用线程退出函数之后, 线程不会马上退出因为当前任务有可能还没有完成, 调回用这个函数是
// 等待任务完成, 然后退出线程, 一般情况下会在 exit() 后边调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);
1.6.2  信号与槽
// 和调用 exit() 效果是一样的
// 代用这个函数之后, 再调用 wait() 函数
[slot] void QThread::quit();
// 启动子线程
[slot] void QThread::start(Priority priority = InheritPriority);
// 线程退出, 可能是会马上终止线程, 一般情况下不使用这个函数
[slot] void QThread::terminate();

// 线程中执行的任务完成了, 发出该信号
// 任务函数中的处理逻辑执行完毕了
[signal] void QThread::finished();
// 开始工作之前发出这个信号, 一般不使用
[signal] void QThread::started();
1.6.3  静态函数
// 返回一个指向管理当前执行线程的QThread的指针
[static] QThread *QThread::currentThread();
// 返回可以在系统上运行的理想线程数 == 和当前电脑的 CPU 核心数相同
[static] int QThread::idealThreadCount();
// 线程休眠函数
[static] void QThread::msleep(unsigned long msecs);    // 单位: 毫秒
[static] void QThread::sleep(unsigned long secs);    // 单位: 秒
[static] void QThread::usleep(unsigned long usecs);    // 单位: 微秒
1.6.4  任务处理函数
// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();

        这个run()是一个虚函数,如果想让创建的子线程执行某个任务,需要写一个子类让其继承QThread,并且在子类中重写父类的run()方法,函数体就是对应的任务处理流程。另外,这个函数是一个受保护的成员函数,不能够在类的外部调用,如果想要让线程执行这个函数中的业务流程,需要通过当前线程对象调用槽函数start()启动子线程,当子线程被启动,这个run()函数也就在线程内部被调用了

1.7  QThread类使用方式

1.7.1  继承QThread类(方式一)
  • 自定义一个继承QThread的类MyThread,重载MyThread中的run()函数,在run()函数中写入需要执行的工作;
  • 调用start()函数来启动线程。

        操作步骤如下:

        1、需要创建一个线程类的子类,让其继承QT中的线程类 QThread

class MyThread:public QThread
{
    ......
}

        2、重写父类的 run() 方法,在该函数内部编写子线程要处理的具体的业务流程

class MyThread:public QThread
{
    ......
 protected:
    void run()
    {
        ........
    }
}

        3、在主线程中创建子线程对象,new 一个就可以了

MyThread * subThread = new MyThread;

        4、启动子线程, 调用 start() 方法

subThread->start();

        示例:

#ifndef MYTHREAD_H
#define MYTHREAD_H
#include<QThread>
#include<QDebug>
class MyThread : public QThread
{
    Q_OBJECT
public:
    MyThread(QObject* parent = nullptr);
signals:                //自定义发送的信号
    void myThreadSignal(const int);
public slots:                //自定义槽
    void myThreadSlot(const int);
protected:
    void run() override;
};

#endif // MYTHREAD_H
#include "mythread.h"

MyThread::MyThread(QObject *parent)
{

}

void MyThread::run()
{
    qDebug()<<"myThread run() start to execute";
    qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
    int count = 0;
    for(int i = 0;i!=1000000;++i)
    {
     ++count;
    }
    emit myThreadSignal(count);
    exec();
}

void MyThread::myThreadSlot(const int val)
{
    qDebug()<<"myThreadSlot() start to execute";
    qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
    int count = 888;
    for(int i = 0;i!=1000000;++i)
    {
     ++count;
    }
}
#include "controller.h"
#include <mythread.h>
Controller::Controller(QObject *parent) : QObject(parent)
{
    myThrd = new MyThread;
    connect(myThrd,&MyThread::myThreadSignal,this,&Controller::handleResults);
    connect(myThrd, &QThread::finished, this, &QObject::deleteLater);            //该线程结束时销毁
    connect(this,&Controller::operate,myThrd,&MyThread::myThreadSlot);

    myThrd->start();
    QThread::sleep(5);
    emit operate(999);
}

Controller::~Controller()
{
    myThrd->quit();
    myThrd->wait();
}

        这种在程序中添加子线程的方式是非常简单的,但是也有弊端,假设要在一个子线程中处理多个任务,所有的处理逻辑都需要写到run()函数中,这样该函数中的处理逻辑就会变得非常混乱,不太容易维护。

1.7.2  QObject::moveToThread(方式二)
  1. 定义一个继承于QObject的worker类,在worker类中定义一个槽slot函数doWork(),这个函数中定义线程需要做的工作;
  2. 在要使用线程的controller类中,新建一个QThread的对象和woker类对象,使用moveToThread()方法将worker对象的事件循环全部交由QThread对象处理;
  3. 建立相关的信号函数和槽函数进行连接,然后发出信号触发QThread的槽函数,使其执行工作。

         操作步骤如下:

        1、创建一个新的类,让这个类从QObject派生

class Work:public QObject
{
    .......
}

        2、在这个类中添加一个公共的成员函数,函数体就是我们要子线程中执行的业务逻辑

class Work:public QObject
{
singals:
    void startwork();

public slots:
    .......
    // 函数名自己指定, 叫什么都可以, 参数可以根据实际需求添加

    void working();
}

        3、在主线程中创建一个QThread对象, 这就是子线程的对象

QThread* t1 = new QThread();

        4、在主线程中创建工作的类对象(千万不要指定给创建的对象指定父对象)

Worker* w1 = new Work(this);    // error
Worker* w1 = new Work();          // ok

        5、将MyWork对象移动到创建的子线程对象中, 调用QObject类提供的moveToThread()方法

// void QObject::moveToThread(QThread *targetThread);
// 如果给work指定了父对象, 这个函数调用就失败了
// 提示: QObject::moveToThread: Cannot move objects with a parent
w1->moveToThread(t1);	// 移动到子线程中工作

        6、启动子线程,调用 start(), 这时候线程启动了, 但是移动到线程中的对象并没有工作

// 启动线程
t1->start();

        7、调用MyWork类对象的工作函数,让这个函数开始执行,这时候是在移动到的那个子线程中运行的

        示例:

#ifndef WORKER_H
#define WORKER_H
#include <QObject>
#include<QDebug>
#include<QThread>
class Worker:public QObject                    //work定义了线程要执行的工作
{
    Q_OBJECT
public:
    Worker(QObject* parent = nullptr){}
public slots:
    void doWork(int parameter)                        //doWork定义了线程要执行的操作
    {
        qDebug()<<"receive the execute signal---------------------------------";
        qDebug()<<"     current thread ID:"<<QThread::currentThreadId();
       for(int i = 0;i!=1000000;++i)
       {
        ++parameter;
       }
       qDebug()<<"      finish the work and sent the resultReady signal\n";
       emit resultReady(parameter);           //emit啥事也不干,是给程序员看的,表示发出信号发出信号
    }

signals:
    void resultReady(const int result);               //线程完成工作时发送的信号
};

#endif // WORKER_H
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <QObject>
#include<QThread>
#include<QDebug>
class Controller : public QObject            //controller用于启动线程和处理线程执行结果
{
    Q_OBJECT
    QThread workerThread;
public:
    Controller(QObject *parent= nullptr);
    ~Controller();
public slots:
    void handleResults(const int rslt)                        //处理线程执行的结果
    {
        qDebug()<<"receive the resultReady signal---------------------------------";
        qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
        qDebug()<<"     the last result is:"<<rslt;
    }
signals:
    void operate(const int);                        //发送信号触发线程
};

#endif // CONTROLLER_H
#include "controller.h"
#include <worker.h>
Controller::Controller(QObject *parent) : QObject(parent)
{
    Worker *worker = new Worker;
    worker->moveToThread(&workerThread);            //调用moveToThread将该任务交给workThread

    connect(this, SIGNAL(operate(const int)), worker, SLOT(doWork(int)));            //operate信号发射后启动线程工作
    connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);            //该线程结束时销毁
    connect(worker, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));            //线程结束后发送信号,对结果进行处理

    workerThread.start();                //启动线程
    qDebug()<<"emit the signal to execute!---------------------------------";
    qDebug()<<"     current thread ID:"<<QThread::currentThreadId()<<'\n';
    emit operate(0);
}

Controller::~Controller()        //析构函数中调用quit()函数结束线程
{
    workerThread.quit();
    workerThread.wait();
}

        使用这种多线程方式,假设有多个不相关的业务流程需要被处理,那么就可以创建多个类似于MyWork的类,将业务流程放多类的公共成员函数中,然后将这个业务类的实例对象移动到对应的子线程中moveToThread()就可以了,这样可以让编写的程序更加灵活,可读性更强,更易于维护。

1.7.3  两种方式比较
  • 子类化QThread的方法,就是重写了QThread中的run()函数,在run()函数中定义了需要的工作。这样的结果是,我们自定义的子线程调用start()函数后,便开始执行run()函数。如果在自定义的线程类中定义相关槽函数,那么这些槽函数不会由子类化的QThread自身事件循环所执行,而是由该子线程的拥有者所在线程(一般都是主线程)来执行。如果你不明白的话,请看,第一个例子中,子类化的线程的槽函数中输出当前线程的ID,而这个ID居然是主线程的ID!!事实的确是如此,子类化的QThread只能执行run()函数中的任务直到run()函数退出,而它的槽函数根本不会被自己的线程执行。
  • moveToThread方法,是把我们需要的工作全部封装在一个类中,将每个任务定义为一个的槽函数,再建立触发这些槽的信号,然后把信号和槽连接起来,最后将这个类调用moveToThread方法交给一个QThread对象,再调用QThread的start()函数使其全权处理事件循环。于是,任何时候我们需要让线程执行某个任务,只需要发出对应的信号就可以。其优点是我们可以在一个worker类中定义很多个需要做的工作,然后发出触发的信号线程就可以执行。相比于子类化的QThread只能执行run()函数中的任务,moveToThread的方法中一个线程可以做很多不同的工作(只要发出任务的对应的信号即可)。

二、线程池基础

        在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在日常开发中,内存资源是及其宝贵的,所以线程池QThreadPool就建议用来管理多个线程的并发执行。在程序逻辑中经常会碰到需要处理大批量任务的情况,比如密集的网络请求,日志分析、加载工程中多个子工程、保存工程等等。一般会创建一个队列,用一个或者多个线程去消费这个队列,一般也要处理队列的加锁和解锁的问题。

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

        Qt多线程的实现方式有:

1. 继承QThread类,重写run()方法

2. 使用moveToThread将一个继承QObject的子类移至线程,内部槽函数均在线程中执行

3. 使用QThreadPool,搭配QRunnable(线程池)

4. 使用QtConcurrent(线程池)

2.1  线程池实现原理

        线程池的组成主要分为3个部分,这三部分配合工作就可以得到一个完整的线程池:

1、任务队列,存储需要处理的任务,由工作的线程来处理这些任务

  • 通过线程池提供的API函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
  • 已处理的任务会被从任务队列中删除
  • 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程

2、工作者线程(任务队列任务的消费者) ,N个

  • 线程池中维护了一定数量的工作线程, 他们的作用是是不停的读任务队列, 从里边取出任务并处理
  • 工作的线程相当于是任务队列的消费者角色,
  • 如果任务队列为空, 工作的线程将会被阻塞 (使用条件变量/信号量阻塞)
  • 如果阻塞之后有了新的任务, 由生产者将阻塞解除, 工作线程开始工作

3、管理者线程(不处理任务队列中的任务),1个

  • 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
  • 当任务过多的时候, 可以适当的创建一些新的工作线程
  • 当任务过少的时候, 可以适当的销毁一些工作的线程

        线程池的优点:

  • 创建和销毁线程需要和OS交互,少量线程影响不大,但是线程数量太大,势必会影响性能,使用线程池可以减少这种开销;
  • 线程池维护一定数量的线程,使用时,将指定函数传递给线程池,线程池会在线程中执行任务

2.2  QRunnab类 [ 不继承QObject,不属于Qt的元对象系统]

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

        一般使用 QThreadPool 在单独的线程中执行代码。使用QRunnable创建线程,步骤如下:

  • 继承QRunnable。和QThread一样, 首先需要将你的线程任务类继承于QRunnable。
  • 重写run函数。和QThread一样,需要重写run函数,run是一个纯虚函数,必须重写。
  • 使用QThreadPool启动线程

        QRunnable 类与QThread类的区别:

  • 与外界通信方式不同。由于QThread是继承于QObject的,但QRunnable不是,所以在QThread线程中可以直接将线程中执行的结果通过信号的方式发到主程序,而QRunnable线程不能用信号槽,只能通过别的方式。
  • 启动线程方式不同。QThread线程可以直接调用start()函数启动,而QRunnable线程需要借助QThreadPool进行启动。
  • 资源管理不同。QThread线程对象需要手动去管理删除和释放而QRunnable则会在QThreadPool调用完成后自动释放

        QRunnable 类特征:

  • 作为Qt类中少有的基类, QRunnable提供了简洁有效的可运行对象的创建. 用QRunnable来创建独立的运行对象来运行 不涉及界面元素的数据处理过程 非常合适.
  • 优点: 创建过程简洁, 使用方便, 配合着自身的autoDelete特性, 有点“招之即来, 挥之即去”的感觉.
  • 缺点: 无法实时提供自身的运行状态.

        QRunnable类 常用函数不多,主要是设置任务对象传给线程池后,是否需要自动析构。

// 在子类中必须要重写的函数, 里边是任务的处理流程
[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 对象使用。

        QThread类特征:        

  • QThreadPool 类管理 QRunnable /QThread 的集合。
  • QThreadPool 管理和回收单独的 QThread 对象,以减少使用线程的程序中的线程创建成本。
  • 每个 Qt 应用程序都有一个全局 QThreadPool 对象,可以通过调用 globalInstance() 来访问它。
  • 要使用 QThreadPool,需要子类化 QRunnable 并实现 run() 虚函数。然后创建该类的对象并将其传递给 QThreadPool::start()。QThreadPool 默认自动删除 QRunnable。
  • QThreadPool 是管理线程的低级类,Qt Concurrent 模块是更高级的方案。

        线程池常用的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);    
}

2.4  QRunnable与外界通信 [QRunnable+QMetaObject::invokeMethod]

        前面我们提到,因为QRunnable没有继承于QObject,所以没法使用信号槽与外界通信,那么,如果要在QRunnable线程中和外界通信怎么办呢,通常有两种做法:

  • 使用多继承。让我们的自定义线程类同时继承于QRunnable和QObject,这样就可以使用信号和槽,但是多线程使用比较麻烦,特别是继承于自定义的类时,容易出现接口混乱,所以在项目中尽量少用多继承。
  • 使用QMetaObject::invokeMethod
2.4.1QMetaObject::invokeMethod介绍

        该函数就是尝试调用obj的member函数,可以是信号、槽或者Q_INVOKABLE声明的函数(能够被Qt元对象系统唤起),如果调用成功,返回true,失败返回false。

//函数定义
[static] bool QMetaObject::invokeMethod(QObject *obj, const char *member, 
                                        Qt::ConnectionType type, QGenericReturnArgument ret,
                                        QGenericArgument val0 = QGenericArgument( Q_NULLPTR ), 
                                        QGenericArgument val1 = QGenericArgument(), 
                                        QGenericArgument val2 = QGenericArgument(), 
                                        QGenericArgument val3 = QGenericArgument(), 
                                        QGenericArgument val4 = QGenericArgument(), 
                                        QGenericArgument val5 = QGenericArgument(), 
                                        QGenericArgument val6 = QGenericArgument(), 
                                        QGenericArgument val7 = QGenericArgument(), 
                                        QGenericArgument val8 = QGenericArgument(), 
                                        QGenericArgument val9 = QGenericArgument())
// QMetaObject::invokeMethod可以是异步调用,也可以是同步调用。这取决与它的连接方式Qt::ConnectionType type。
// 如果type为Qt::DirectConnection,则为同步调用,若为Qt::QueuedConnection,则为异步调用。

        示例:

        假如在主界面中定一个函数 [该函数为QObject类型中一个成员函数] ,用于更新界面内容:

Q_INVOKABLE void setText(QString msg);

线程类:

// .h
class CusRunnable : public QRunnable
{
public:
    explicit CusRunnable(QObject *obj);
    ~CusRunnable();
    void run();
 
private:
    QObject * m_pObj = nullptr;//主界面需要刷新对象,即setText()对应的类对象
};


// .cpp
CusRunnable::CusRunnable(QObject * obj):
    m_pObj(obj)
{}
 
CusRunnable::~CusRunnable()
{
    qDebug() << __FUNCTION__;
}
 
void CusRunnable::run()
{
    qDebug() << __FUNCTION__ << QThread::currentThreadId();
    
  //其中"setText"就是要调用的函数,
  //传参方式Q_ARG(QString,"this is AA!"),表示传入一个QString类型,值为"this is AA!"
  QMetaObject::invokeMethod(m_pObj,"setText",Q_ARG(QString,"this is AA!"));
    QThread::msleep(1000);
}

2.5 QtConcurrent类(线程池)(方式四)

        QtConcurrent命名空间提供了高级API,使得可以在不使用低级线程原语(例如:互斥、读写锁、等待条件或信号量)的情况下编写多线程程序,例如子类化QThread、QObject::moveToThread()、子类化QRunnable对于共享数据的保护都要使用低级线程原语,这无疑是要非常小心的。
        使用 QtConcurrent 编写的程序根据可用的处理器核心数自动调整所使用的线程数。这意味着,当在未来部署多核系统时,现在编写的应用程序将继续适应。

        只要在 pro 文件添加“Qt += concurrent”并且在我们的 h 文件添加“#include <QtConcurrent>”,就可以使用这些函数了

2.5.1使用concurrent命名空间中的函数
QFuture<T> QtConcurrent::run(Function function, ...)
QFuture<T> QtConcurrent::run(QThreadPool *pool, Function function, ...)

        function函数会在一个单独线程中进行,并且该线程取自全局QThreadPool。返回的QFuture可以用来查询函数是否运行、完成状态和function的返回值。返回的QFuture就可以使用QFutureWatch监视,当QFuture的状态发生改变时,监视者就可以监视这个回调从而对异步运行的结果进行处理。

2.5.2  run函数

        run函数是通过另开一个线程来执行用户指定的函数,需要注意的是这个线程是在线程池中获取的,也就是说这个线程是不需要手动释放的,运行完指定的函数线程会自动释放。

//自定义函数
QString hello(QString name,QString name1,QString name2,QString name3){
    qDebug() << "hello" <<name << "from" <<QThread::currentThread();

    for(int i=0; i<3; i++){
        QThread::sleep(1);
        qDebug("[%s] i = %d",name.data(),i);
    }

    return name+name1+name2+name3;
}
//run函数在线程中调用自定义的函数,后面可以跟函数的参数(可选),QFuture<returnType>能够获取到函数执行的返回值,returnType必须和函数的返回类型一致
	QFuture<QString> f1 = QtConcurrent::run(hello,QString("Alice"),QString("Alice"),QString("Alice"),QString("Alice"));

	QFuture<QString> f2 = QtConcurrent::run(hello,QString("Bob"),QString("Bob"),QString("Bob"),QString("Bob"));
	//QFuture::result()获取单个返回值
	qDebug() << f1.result();
	qDebug() << f2.result();
	//等待结束释放
	f1.waitForFinished();
    f2.waitForFinished();
2.5.3  map函数
map 函数用于需要更改原容器中数据的使用场景,对容器中的每个项目都调用一次函数,且每次调用都是单独的一个线程。这个没有返回新容器,所以不能通过future获取结果。线程也来自线程池,和run方法类似,不需要手动释放。
  • QtConcurrent::map() :对序列的每一项元素都应用一个函数,并将运算结果替换原来的元素。
  • QtConcurrent::mapped() :功能类似 map() 函数,它会返回一个新容器存储函数处理后的结果。
  • QtConcurrent::mappedReduced() :类似于 mapped() ,他会将这个返回的结果序列,经过另一个函数处理为一个单个的值。
 void toUpperMap(QString &str)
 {
     str = str.toUpper();
 }
 
 	QStringList strWords;
    strWords << "Apple" << "Banana" << "cow" << "dog" << "Egg";
    //第一个参数是原容器,第二参数是每个项目需要调用的方法
    auto future = QtConcurrent::map(strWords,toUpperMap);
    future.waitForFinished();
    qDebug() << strWords;
    //输出:QList("APPLE", "BANANA", "COW", "DOG", "EGG")



QString toUperrMapped(const QString &str){
     return str.toUpper();
 }
 
	QStringList oldStr;
    oldStr << "Apple" << "Banana" << "cow" << "dog" << "Egg";
    auto future = QtConcurrent::mapped(oldStr,toUperrMapped);
    future.waitForFinished();
    qDebug() << future.results();//results()返回全部数据,这里如果调用result()只会返回一个数据,即APPLE。
    //输出:QList("APPLE", "BANANA", "COW", "DOG", "EGG")

2.5.4  filter函数

        filter函数与map函数类似,其衍生函数也与map的衍生函数类似,只是函数用于过滤。

  • QtConcurrent::filter() :直接操作容器,处理函数必须是“bool function(const T &t)”的形式。返回为 true 的元素会保留,反之会从容器中删除。
  • QtConcurrent::filtered() :不直接操作容器,将结果以新容器返回,处理函数和 filter() 一样。
  • QtConcurrent::filteredReduced() :类似于 filtered(),将进一步把结果处理成一个单一值。处理函数的形式必须是“V function (T &result, const U &intermediate)”。
 void reduceFun(QList<QString> &dictionary,const QString &string){
     dictionary.push_back(QString("result:")+string);
 }

 bool fiter (QString string){
     if(string.length()>3){ //只要长度大于3的字符串
         return true;
     }else return false;
 }
	QStringList oldStrfilter;
    oldStrfilter << "Apple" << "Banana" << "cow" << "dog" << "Egg";
    auto future1 = QtConcurrent::filter(oldStrfilter,fiter);

    //filtered函数与mapped函数类似,只是函数用于过滤
    auto future2 = QtConcurrent::filtered(oldStrfilter,fiter);

    //filteredReduced函数与mappedReduced函数类似,只是函数用于过滤
    auto future3 = QtConcurrent::filteredReduced(oldStrfilter,fiter,reduceFun);
	future1.waitForFinished();
    future2.waitForFinished();
    future3.waitForFinished();
    
	qDebug() << oldStrfilter;
    qDebug() << future2.results();
    qDebug() << future3.results();
/**
输出:
QList("Apple", "Banana")
QList("Apple", "Banana")
QList(QList("result:Apple", "result:Banana"))
**/

        总结

        QtConcurrent::run,在线程池内起一个线程来执行一个函数。
        QtConcurrent::map, 用于并行处理一批数据的场景。
        QtConcurrent::filter,一般用于对一批数据的过滤操作。

        使用QtConcurrent处理一些需要在并行线程中完成的计算,比使用QThread更方便(使用QThread要自己加锁的)。

       使用场景  

       若有大量工作需要完成,则使用方式1、2、3均可,但是若只有一小段工作,需要在线程中完成,无论是使用QThread,还是moveToThread,更或者,使用QThreadPool,都有大材小用的感觉,这时候,使用 QtConcurrent 就是最佳选择

        示例

QT      += concurrent
1 //main.cpp
 2 #include <QCoreApplication>
 3 #include <QThread>
 4 #include <QThreadPool>
 5 #include <QtConcurrent/QtConcurrent>
 6 #include <QDebug>
 7 #include <QFuture>
 8 
 9 static QThreadPool* g_pThreadPool = QThreadPool::globalInstance();
10 
11 class HELLO
12 {
13 public:
14     QString hello(QString szName)
15     {
16         qDebug() << "Hello " << szName << " from " << QThread::currentThreadId();
17         return szName;
18     }
19 
20     void run()
21     {
22         QFuture<QString> f3 = QtConcurrent::run(this, &HELLO::hello, QString("Lily"));
23         QFuture<QString> f4 = QtConcurrent::run(g_pThreadPool, this, &HELLO::hello, QString("Sam"));
24 
25         f3.waitForFinished();
26         f4.waitForFinished();
27 
28         qDebug() << "f3 : " << f3.result();
29         qDebug() << "f4 : " << f4.result();
30     }
31 };
32 
33 QString hello(QString szName)
34 {
35     qDebug() << "Hello " << szName << " from " << QThread::currentThreadId();
36     return szName;
37 }
38 
39 int main(int argc, char *argv[])
40 {
41     QCoreApplication a(argc, argv);
42 
43     QFuture<QString> f1 = QtConcurrent::run(hello, QString("Alice"));
44     QFuture<QString> f2 = QtConcurrent::run(g_pThreadPool, hello, QString("Bob"));
45 
46     f1.waitForFinished();
47     f2.waitForFinished();
48 
49     qDebug() << "f1 : " << f1.result();
50     qDebug() << "f2 : " << f2.result();
51 
52     HELLO h;
53     h.run();
54 
55     g_pThreadPool = NULL;
56 
57     return a.exec();
58 }

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值