Qt - 多线程

1. 线程概念的起源

1.1 单核 CPU

在早期的单核 CPU 时代还没有线程的概念,只有进程。操作系统作为一个大的“软件”,协调着各个硬件(如CPU、内存,硬盘、网卡等)有序的工作着。在双核 CPU 诞生以前,我们用的 Windows 操作系统依然可以一边用 word 写文档一边听着音乐,作为整个系统唯一可以完成计算任务的 CPU 是如何保证两个进程“同时进行”的呢?时间片轮转调度

注意这个关键字「轮转」。每个进程会被操作系统分配一个时间片,即每次被 CPU 选中来执行当前进程所用的时间。时间一到,无论进程是否运行结束,操作系统都会强制将 CPU 这个资源转到另一个进程去执行。为什么要这样做呢?因为只有一个单核 CPU,假如没有这种轮转调度机制,那它该去处理写文档的进程还是该去处理听音乐的进程?无论执行哪个进程,另一个进程肯定是不被执行,程序自然就是无运行的状态。如果 CPU 一会儿处理 word 进程一会儿处理听音乐的进程,起初看起来好像会觉得两个进程都很卡,但是 CPU 的执行速度已经快到让人们感觉不到这种切换的顿挫感,就真的好像两个进程在“并行运行”。

img

如上图所示,每一个小方格就是一个时间片,大约100ms。假设现在我同时开着 Word、QQ、网易云音乐三个软件,CPU 首先去处理 Word 进程,100ms时间一到 CPU 就会被强制切换到 QQ 进程,处理100ms后又切换到网易云音乐进程上,100ms后又去处理 Word 进程,如此往复不断地切换。我们将其中的 Word 单独拿出来看,如果时间片足够小,那么以人类的反应速度看就好比最后一个处理过程,看上去就会有“CPU 只处理 Word 进程”的幻觉。随着芯片技术的发展,CPU 的处理速度越来越快,在保证流畅运行的情况下可以同时运行的进程越来越多。

1.2 多核 CPU

随着运行的进程越来越多,人们发现进程的创建、撤销与切换存在着较大的时空开销,因此业界急需一种轻型的进程技术来减少开销。于是上世纪80年代出现了一种叫 SMP(Symmetrical Multi-Processing)的对称多处理技术,就是我们所知的线程概念。线程切换的开销要小很多,这是因为每个进程都有属于自己的一个完整虚拟地址空间,而线程隶属于某一个进程,与进程内的其他线程一起共享这片地址空间,基本上就可以利用进程所拥有的资源而无需调用新的资源,故对它的调度所付出的开销就会小很多。

img

以 QQ 聊天软件为例,上文我们一直都在说不同进程如何流畅的运行,此刻我们只关注一个进程的运行情况。如果没有线程技术的出现,当 QQ 这个进程被 CPU “临幸”时,我是该处理聊天呢还是处理界面刷新呢?如果只处理聊天,那么界面就不会刷新,看起来就是界面卡死了。有了线程技术后,每次 CPU 执行100ms,其中30ms用于处理聊天,40ms用于处理传文件,剩余的30ms用于处理界面刷新,这样就可以使得各个组件可以“并行”的运行了。于是乎我们可以提炼出两点关于多线程的适用场景:

  • 通过使用多核 CPU 提高处理速度。

  • 保证 GUI 界面流畅运行的同时可以执行其他计算任务。

2. Qt线程操作

Qt的线程类为QThread,是独立于平台的线程操作类。

2.1 公共函数

//返回指向线程的事件调度程序对象的指针。如果该线程不存在事件分派器,则此函数返回nullptr.l
QAbstractEventDispatcher *eventDispatcher() const
//告诉线程的事件循环退出并返回代码。
void exit(int returnCode = 0)
//如果线程完成返回true;否则返回false。
bool isFinished() const
//如果在此线程上运行的任务应该停止,则返回true。可以通过requestinterrupt()请求中断。
bool isInterruptionRequested() const
//如果线程正在运行则返回true;否则返回false。
bool isRunning() const
//返回线程的当前事件循环级别。
int loopLevel() const
//返回正在运行的线程的优先级。如果线程没有运行,这个函数返回InheritPriority。
QThread::Priority priority() const
//请求线程的中断。该请求是建议性的,由线程上运行的代码决定是否以及如何对该请求采取行动。此函数不会停止线程上运行的任何事件循环,也不会以任何方式终止它。
void requestInterruption()
//将线程的事件分派器设置为eventDispatcher。这只有在还没有为线程安装事件分派器的情况下才有可能。也就是说,在线程被start()启动之前,或者在主线程的情况下,在QCoreApplication被实例化之前。这个方法获得了对象的所有权。
void setEventDispatcher(QAbstractEventDispatcher *eventDispatcher)
//这个函数为正在运行的线程设置优先级。如果线程没有运行,这个函数什么都不做并立即返回。使用start()启动具有特定优先级的线程。
void setPriority(QThread::Priority priority)
//将线程的最大堆栈大小设置为stackSize。如果stackSize大于0,则最大堆栈大小设置为stackSize字节,否则由操作系统自动确定最大堆栈大小。
void setStackSize(uint stackSize)
//返回线程的最大堆栈大小(如果使用setStackSize()设置);否则返回零。
uint stackSize() const
//阻塞线程,直到满足这些条件中的任何一个
bool wait(unsigned long time = ULONG_MAX)

 

2.2 静态公共函数

//创建线程,创建一个新的QThread对象,它将执行函数f。
[static] QThread *create(Function &&f)
//获取当前线程对象地址
[static] QThread *QThread::currentThread()
//获取可在系统上运行的理想线程数。 这是通过查询系统中实际的和逻辑的处理器核数来完成的。 如果无法检测到处理器核数,则该函数返回1。 
[static] int QThread::idealThreadCount()
//强制当前线程休眠n毫秒。      
[static] void QThread::msleep(unsigned long msecs)   
//强制当前线程休眠n秒  
[static] void QThread::sleep(unsigned long secs)    
//强制当前线程休眠n微秒      
[static] void QThread::usleep(unsigned long usecs)  
//放弃执行当前线程, 把机会让给别的线程,注意,操作系统决定切换到哪个线程。  
[static] void QThread::yieldCurrentThread()

 

2.3 信号与槽

信号:

[signal] void QThread::started()//这个信号在相关线程开始执行时,在run()函数被调用之前发出。

[signal] void QThread::finished()//这个信号在相关线程完成执行之前从它发出

 

示例:

信号执行完毕,该信号可以连接到QObject::deleteLater(),以释放该线程中的对象。

void fun()
{
    for(int i =0;i<100;i++)
    {
        QThread::msleep(20);
        qDebug()<<"i:"<<i;
    }
}

    QThread *thr =  QThread::create(fun);
    QObject::connect(thr,&QThread::started,[](){qDebug()<<"start";});
    QObject::connect(thr,&QThread::finished,[=](){thr->deleteLater();qDebug()<<"finished";});
    thr->start();

 

槽函数:

[slot] void QThread::quit()//告诉线程的事件循环以返回码0 (success)退出。

[slot] void QThread::start(Priority priority = InheritPriority)//通过调用run()开始执行线程。操作系统将根据优先级参数调度线程。如果线程已经在运行,这个函数什么也不做。

[slot] void QThread::terminate()//终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调度策略。

 

3. 使用多线程

Qt 使用多线程有三种方式

 

3.1 第一种:静态函数

注:在 Qt 5.10 及更高版本中可以这样来创建一个线程,可以使用QThread::create()来直接创建一个线程

QThread有两个静态成员函数create,创建一个新的QThread对象,它将使用参数args执行函数f。(必须在C++17及以上才可以使用)

[static] QThread *QThread::create(Function &&f, Args &&... args)    //C++17
[static] QThread *QThread::create(Function &&f)

新线程没有启动——它必须通过显式调用start()来启动。 这允许您连接到它的信号,将QObjects移动到线程,选择新线程的优先级,等等。 函数f将在新线程中被调用。

注意:不要对返回的QThread实例多次调用start(); 这样做将导致未定义的行为。

全局、类的静态、非静态函数做入口函数

void print()
{
    for(int i = 0;i<5;i++)
        qInfo()<<"hello global print";
}
class MainWindow:public QWidget
{
    Q_OBJECT
public:
    MainWindow(QWidget*parent = nullptr):QWidget(parent)
    {
        qInfo()<<"end";
    }
    static void print1()
    {
        for(int i = 0;i<5;i++)
            qInfo()<<"hello static print1";
    }
    void print2()
    {
        for(int i = 0;i<5;i++)
            qInfo()<<"hello print2";
    }
};
  • QThread::create使用全局函数作为入口函数。

auto* thr = QThread::create(print);
thr->start();
  • QThread::create使用类的静态函数作为入口函数。

auto* thr1 = QThread::create(&MainWindow::print1);
thr1->start();
  • QThread::create使用类的成员函数作为入口函数。

auto* thr2 = QThread::create(&MainWindow::print2,&w);
thr2->start();
  • QThread::create使用Lambda表达式作为入口函数。

auto* thr3 = QThread::create([](){qInfo()<<"hello lambda";});
thr3->start();

 

给入口函数传参

void print(const QString& str)
{
    for(int i = 0;i<5;i++)
    {
        qInfo()<<"hello global print"<<str;
    }
}
​
auto* thr = QThread::create(print,"I Like Qt");
thr->start();

 

3.2 第二种:继承QThread

QThread类中有一个virtual函数QThread::run(),要创建一个新的线程,我们只需定义一个MyThread类,让其继承QThread,然后重新实现QThread::run()。

run函数是线程的起始点。 在调用start()之后,新创建的线程调用这个函数。 默认实现只是调用exec()。

头文件:MyThread.h

#include <QThread>

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);
    ~MyThread();
    
protected:
    void run() override;
    
signals:
};

源文件:MyThread.cpp

#include "mythread.h"
#include <QDebug>

MyThread::MyThread(QObject *parent): QThread(parent)
{
    
}

MyThread::~MyThread()
{
    qDebug()<<   "MyThread::~MyThread()";
}

void MyThread::run()
{
    int i= 0;
    while(i<200)
    {
        QThread::msleep(20);
        if(i==150)
            break;
        qDebug()<<"i"<<i++;
    }
}

源文件:Widget.h

#include <QWidget>
#include "mythread.h"

namespace Ui {
class Widget;
}

class Widget: public QWidget
{
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
    
private:
    Ui::Widget *ui;
    MyThread* mythread;
};

源文件:Widget.cpp

#include "widget.h"
#include<QDebug>
#include<QThread>

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{
    ui->setupUi(this);
    mythread = new MyThread(this);//创建线程
    connect(mythread,&QThread::started,this,[](){qDebug()<<"start";});
    connect(mythread,&QThread::finished,this,[](){qDebug()<<"finished";});
    mythread->start();//启动线程
}

Widget::~Widget()
{
    mythread->quit();//退出线程
    delete mythread;
    delete ui;
}

 

3.3 第三种:移动到线程

QThread是被设计来作为一个操作系统线程的接口和控制点,而不是用来写入你想在线程里执行的代码的地方。我们(面向对象程序员)编写子类,是因为我们想扩充或者特化基类中的功能。我唯一想到的继承QThread类的合理原因,是添加QThread中不包含的功能,比如,也许可以提供一个内存指针来作为线程的堆栈,或者可以添加实时的接口和支持。用于下载文件、查询数据库,或者做任何其他操作的代码都不应该被加入到QThread的子类中;它应该被封装在它自己的对象中。

你可以简单地把类从继承QThread改为继承QObject。为了让你的代码实际运行在新线程的作用域中,你需要实例化一个QThread对象,并且使用moveToThread()函数将你的对象分配给它。你同过moveToThread()来告诉Qt将你的代码运行在特定线程的作用域中,让线程接口和代码对象分离。

暂时不考虑多线程,先思考这样一个问题:想想我们平时会把耗时操作代码放在哪里?一个类中。那么有了多线程后,难道我们要把这段代码从类中剥离出来单独放到某个地方吗?显然这是很糟糕的做法。QObject 中的 moveToThread() 函数可以在不破坏类结构的前提下依然可以在新线程中运行

 

案例1

移动线程步骤:

  1. 创建一个新的类(工作类对象MyWork),必须继承自QObject
  2. 在类中添加一个成员函数,函数名称、签名等可自定义
  3. 在主线程中创建一个QThread对象QThread* sub = new QThread;
  4. 在主线程中创建工作类对象,千万不能指定父对象 MyWork* work = new MyWork
  5. 将工作类对象移动到子线程中work->moveToThread(sub)
  6. 启动子线程sub.start(),但是线程并没有工作
  7. 调用工作类对象中的工作函数,子线程开始工作

mywork.h

//mywork.h

#ifndef MYWORK_H
#define MYWORK_H

#include <QObject>

class MyWork : public QObject
{
    Q_OBJECT
public:
    explicit MyWork(QObject *parent = nullptr);

    // 工作函数
    void working();

signals:
    void curNumber(int num);

public slots:
};

#endif // MYWORK_H

mywork.cpp

//mywork.cpp

#include "mywork.h"
#include <QDebug>
#include <QThread>

MyWork::MyWork(QObject *parent) : QObject(parent)
{

}

void MyWork::working()
{
    qDebug() << "当前线程对象的地址: " << QThread::currentThread();

    int num = 0;
    while(1)
    {
        emit curNumber(num++);
        if(num == 10000000)
        {
            break;
        }
        QThread::usleep(1);
        qDebug()<<"num = "<<num;
    }
    qDebug() << "run() 执行完毕, 子线程退出...";
}

mainwindow.cpp

//mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QThread>
#include "mywork.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    qDebug() << "主线程对象的地址: " << QThread::currentThread();

    // 创建线程对象
    QThread* sub = new QThread;
    // 创建工作的类对象
    // 千万不要指定给创建的对象指定父对象
    // 如果指定了: QObject::moveToThread: Cannot move objects with a parent
    MyWork* work = new MyWork;
    // 将工作的类对象移动到创建的子线程对象中
    work->moveToThread(sub);
    // 启动线程
    sub->start();
    // 让工作的对象开始工作, 点击开始按钮, 开始工作
    connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);
    // 显示数据
    connect(work, &MyWork::curNumber, this, [=](int num)
    {
        ui->label->setNum(num);
    });
}

MainWindow::~MainWindow()
{
    delete ui;
}

执行结果:

线程资源释放

  1. 直接利用对象树,在创建对象的时候制定父对象
  2. 直接析构,如果是在函数范围内声明的QThread对象,析构函数不能够访问可以这样
connect(this,&MainWindow::destroy,this,[=](){//主窗体析构的时候会触发destroy信号
    t1->quit();
    t1->wait();
    t1->deleteLater();
});

 

案例2

通常处理ui卡死最常用的办法,就是将耗时操作放进线程中执行,等待线程执行完毕后发信号出来通知,当前耗时操作已经被执行完,允许进入下一环节。
Qt线程创建的方式有两种,一种是继承至QThread,之后重写run()方法来实现线程的创建,另一种是继承至QObject,通过moveToThread的方式创建,官方推荐使用后者,两者的区别是:
moveToThread 方法,是把我们需要的工作全部封装在一个类中,将每个任务定义为一个槽函数,再建立触发这些槽函数的信号,然后连接信号和槽,最后调用 moveToThread 方法将这个类交给一个 QThread 对象,再调用 QThread 的 start() 函数使其全权处理事件循环。于是,任何时候我们需要让子线程执行某个任务,只需要发出对应的信号就可以。
其优点是我们可以在一个worker类中定义很多个需要做的工作,然后触发信号,子线程就可以执行。相比于继承 QThread 方法,只能执行 run() 函数中的任务,moveToThread 的方法中一个线程可以做很多不同的工作,只要实现对应的槽函数,触发对应的信号即可,针对第二种方法,有如下代码:
 
Worker.h 文件
#ifndef DEL_WORKER_H
#define DEL_WORKER_H

#include <QObject>
#include <QDebug>
#include <QThread>

class Worker : public QObject
{
Q_OBJECT

public:
    explicit Worker(QObject *parent = nullptr)
    {
    
    }

public slots:
    void doWork(int parameter);  // doWork 定义了线程要执行的操作
    {
        qDebug() << "receive the execute signal" ;
        qDebug() << "\tCurrent thread ID: " << QThread::currentThreadId();

    // 循环一百万次
        for (int i = 0; i != 1000000; ++i)
        {
            ++parameter ;
        }

        // 发送结束信号
        qDebug() << "\tFinish the work and sent the result Ready signal\n" ;
        emit resultReady(parameter);
    }

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

#endif //DEL_WORKER_H
Controller.h 文件
#include "Worker.h"

// controller 用于 启动子线程 和 处理子线程执行的结果
class Controller : public QObject
{
    Q_OBJECT

    QThread workerThread ;

public:
    explicit Controller(QObject *parent = nullptr)
    {
        auto *worker = new Worker ;

        // 调用 moveToThread 将该任务交给 workThread
        worker->moveToThread(&workerThread);

        // operate 信号发射后启动线程工作
        connect(this, SIGNAL(operate(const int)), worker, SLOT(doWork(int)));

        // 该线程结束时销毁
        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() << "\tCurrent thread ID:" << QThread::currentThreadId() << '\n' ;

        emit operate(0);
    }

    ~Controller()
    {
        workerThread.quit();
        workerThread.wait();
    };

public slots:
    static void handleResults(int result)  // 处理子线程执行的结果
    {
        qDebug() << "receive the resultReady signal" ;
        qDebug() << "\tCurrent thread ID: " << QThread::currentThreadId() << '\n' ;
        qDebug() << "\tThe last result is: " << result ;
    }

signals:
    void operate(const int);  // 发送信号,触发线程

};

#endif //DEL_CONTROLLER_H

小知识

1.在主线程退出之前,析构函数中总是需要先停掉子线程,这是比较合理的操作,否则系统报错误:主线程先于子线程退出
2.QThread中的静态方法currentThreadId()可以返回当前线程句柄
3.QtConCurrent的线程是线程安全型的

 

4. 启动线程前的准备工作

4.1 开多少个线程比较合适?

说“开线程”其实是不准确的,这种事儿只有操作系统才能做,我们所能做的是管理其中一个线程。无论是 QThread thread 还是 QThread *thread,创建出来的对象仅仅是作为操作系统线程的接口,用这个接口可以对线程进行一些操作。虽然这样说不准确,但下文我们仍以“开线程”的说法,只是为了表述方便。

我们来思考这样一个问题:“线程数是不是越大越好”?显然不是,“开”一千个线程是没有意义的。线程的切换是要消耗系统资源的,频繁的切换线程会使性能降低。线程太少的话又不能完全发挥 CPU 的性能。一般后端服务器都会设置最大工作线程数,不同的架构师有着不同的经验,有些业务设置为 CPU 逻辑核心数的4倍,有的甚至达到32倍。

Venkat Subramaniam 博士的《Programming Concurrency on the JVM》这本书中提到关于最优线程数的计算,即线程数量 = 可用核心数/(1 - 阻塞系数)。可用核心数就是所有逻辑 CPU 的总数,这可以用 QThread::idealThreadCount() 静态函数获取,比如双核四线程的 CPU 的返回值就是4。但是阻塞系数比较难计算,这需要用一些性能分析工具来辅助计算。如果只是粗浅的计算下线程数,最简单的办法就是 CPU 核心数 * 2 + 2。更为精细的找到最优线程数需要不断的调整线程数量来观察系统的负载情况。

4.2 设置栈大小

每个线程都有自己的栈,彼此独立,由编译器分配。一般在 Windows 的栈大小为2M,在 Linux 下是8M。

Qt 提供了获取以及设置栈空间大小的函数:stackSize()、setStackSize(uint stackSize)。其中 stackSize() 函数不是返回当前所在线程的栈大小,而是获取用 stackSize() 函数手动设置的栈大小。如果是用编译器默认的栈大小,该函数返回0,这一点需要注意。为什么要设置栈的大小?这是因为有时候我们的局部变量很大(常见于数组),当超过编译器默认大小时程序就会因为栈溢出而报错,这时候就需要手动设置栈大小了。

在 Windows操作系统环境下,假如我们在线程入口函数fun()中添加一个3M大小的数组 array,可以看出在程序运行时会由于栈溢出而导致异常退出,因为 Windows默认的栈空间仅为2M。

如果我们设置了栈大小为4M,那么程序会正常运行,不会出现栈溢出的问题。

 

5. 启动线程/退出线程

5.1 启动线程

start()

调用 start() 函数就可以启动函数在新线程中运行,运行后会发出 started() 信号。

在上面的使用线程中我们知道将耗时函数放入新线程有QThread::create()、 moveToThread() 和继承 QThread 且重新实现 run() 函数三种方式。有这么一种情况:此时我有 fun1()fun2() 两个耗时函数,将 fun1() 中的代码放入 run() 函数,而将 fun2() 以 moveToThread() 的方式也放到这个线程中。那新线程该运行哪个函数呢?其实调用 start() 函数后,新线程会优先执行 run() 中的代码,即先执行 fun1() 函数,其次才会运行 fun2() 函数。这种情况不常见,但了解这种先后顺序有助于我们理解 start() 函数。

说到 run() 函数就不得不提 exec() 函数。这是个 protected 函数,因此只能在类内使用。默认 run() 函数会调用 exec() 函数,即启用一个局部的不占 CPU 的事件循环。为什么要默认启动个事件循环呢?这是因为没有事件循环的话,耗时代码只要执行完线程就会退出,频繁的开销线程显然很浪费资源。因此,如果使用上述第二种“开线程”的方式,别忘了在 run() 函数中调用 exec() 函数。

 

5.2 退出线程

quit()

退出线程可是个技术活,不是随随便便就可以退出。比如我们关闭主进程的同时,里面的线程可能还处在运行状态,尤其线程上跑着耗时操作。这时候你可以用 terminate() 函数强制终止线程,调用该函数后所有处于等待状态的线程都会被唤醒。该函数是异步的,也就是说调用该函数后虽然获得了返回值,但此时线程依然可能在运行。因此,一般是在后面跟上 wait() 函数来保证线程已退出。当然强制是很暴力的行为,有可能会造成局部变量得不到清理,或者无法解锁互斥关系,种种行为都是很危险的,除非必要时才会使用该函数。

在上面我们说到默认 run() 函数会调用 exec() 函数,耗时操作代码执行完后,线程由于启动了事件循环是不退出的。所以,正常的退出线程其实质是退出事件循环,即执行 exit(int returnCode = 0) 函数。返回0代表成功,其他非零值代表异常。quit() 函数等价于 exit(0)。线程退出后会发出 finished() 信号。

 

6. 操作运行中的线程

6.1 获取状态

(1)运行状态

线程的状态有很多种,而往往我们只关心一个线程是运行中还是已经结束。QThread 提供了 isRunning()、isFinished() 两个函数来判断当前线程的运行状态。

(2)线程标识

关于 currentThreadId() 函数,很多人将该函数用于输出线程ID,这是错误的用法。该函数主要用于 Qt 内部,不应该出现在我们的代码中。那为什么还要开放这个接口?这是因为我们有时候想和系统线程进行交互,而不同平台下的线程 ID 表示方式不同。因此调用该函数返回的 Qt::HANDLE 类型数据并转化成对应平台的线程 ID 号数据类型(例如 Windows 下是 DWORD 类型),利用这个转化后的 ID 号就可以与系统开放出来的线程进行交互了。当然,这就破坏了移植性了。

需要注意的是,这个 Qt::HANDLE 是 ID 号而不是句柄。句柄相当于对象指针,一个线程可以被多个对象所操控,而每个线程只有一个全局线程 ID 号。正确的获取线程 ID 做法是:调用操作系统的线程接口来获取。以下是不同平台下获取线程 ID 的代码:

#include <QCoreApplication>
#include <QDebug>

#ifdef Q_OS_LINUX
#include <pthread.h>
#endif

#ifdef Q_OS_WIN
#include <windows.h>
#endif

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

#ifdef Q_OS_LINUX
    qDebug() << pthread_self();
#endif

#ifdef Q_OS_WIN
    qDebug() << GetCurrentThreadId();
#endif

    return a.exec();
}

我们自己的程序内部可以调用 currentThread() 函数来获取 QThread 指针,有了线程指针就可以对线程进行一些操作了。

(3)更为精细的事件处理

在《Qt 中的事件系统 - 有什么难的呢?》一文中我们提到事件的整个运行流程,文中所提及的 QCoreApplication::processEvents() 等传递事件方法其实是很简单的,但如果再深入下去就无能为力了。Qt 提供了 QAbstractEventDispatcher 类用于更为精细的事件处理,该类精细到可以管理 Qt 事件队列,即接收到事件(来自操作系统或者 Qt 写的程序)后负责发送到 QCoreApplication 或者 QApplication 实例以进行处理。而文中讲的是从 QCoreApplication 接收到事件开始,再往后的事情了。

线程既然可以开启事件循环,那么就可以调用 eventDispatcher()、setEventDispatcher() 函数来设置和获取事件调度对象,然后对事件进行更为精细的操作。

除此以外,loopLevel() 函数可以获取有多少个事件循环在线程中运行。正如下文所说,这个函数本来在 Qt 4 中被删除了,但是对于那些想知道有多少事件循环的人来说该函数还是有用的。所以在 Qt 5 中又加了进来。

This function used to reside in QEventLoop in Qt 3 and was deprecated in Qt 4. However this is useful for those who want to know how many event loops are running within the thread so we just make it possible to get at the already available variable.

6.2 操作线程

(1)安全退出线程必备函数:wait(unsigned long time = ULONG_MAX)

在上面已经提到“一般是在后面跟上 wait() 函数来保证线程已退出”,线程退出的时候不要那么暴力,告诉操作系统要退出的线程后,给点时间(即阻塞)让线程处理完。也可以设置超时时间 time,时间一到就强制退出线程。一般在类的析构函数中调用,正如本文开头「2.1 我们该把耗时代码放在哪里?」的示例代码那样:

Controller::~Controller()
{
    m_workThread.quit();
    m_workThread.wait();
}

(2)线程间的礼让行为

这是个很有意思的话题,一般我们都希望每个线程都能最大限度的榨干系统资源,何来礼让之说呢?有时候我们采用多线程并不只是运行耗时代码,而是和主 GUI 线程分开,避免主界面卡死的情况发生。那么有些线程上跑的任务可能对实时性要求不高,这时候适当的缩短被 CPU 选中的机会可以节约出系统资源

img

除了调用 setPriority()、priority() 优先级相关的函数以外,QThread 类还提供了 yieldCurrentThread() 静态函数,该函数是在通知操作系统“我这个线程不重要,优先处理其他线程吧”。当然,调用该函数后不会立马将 CPU 计算资源交出去,而是由操作系统决定。

QThread 类还提供了 sleep()、msleep()、usleep() 这三个函数,这三个函数也是在通知操作系统“在未来 time 时间内我不参与 CPU 计算”。从我们直观的角度看,就好像当前线程“沉睡”了一段时间。

(3)线程的中断标志位

Qt 为每一个线程都设置了一个布尔变量用来标记当前线程的终端状态,用 isInterruptionRequested() 函数来获取,用 requestInterruption() 函数来设置中断标记。这个标记不是给操作系统看的,而是给用户写的代码中进行判断。也就是说调用 requestInterruption() 函数并不能中断线程,需要我们自己的代码去判断。这有什么用处呢?

while (ture) {
    if (!isInterruptionRequested()) {
        // 耗时操作
        ......
    }
}

这种设计可以让我们自助的中断线程,而不是由操作系统强制中断。经常我们会在新线程上运行无限循环的代码,在代码中加上判断中断标志位可以让我们随时跳出循环。好处就是给了我们程序更大的灵活性

 

7. 为每个线程提供独立数据

思考这样一个问题,如果线程本身存在全局变量,那么修改一处后另一个线程会不会受影响?我们以一段代码为例:

// main.cpp
#include <QCoreApplication>
#include "workthread.h"

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

    WorkThread thread1;
    WorkThread thread2;

    thread1.start();
    thread2.start();

    return a.exec();
}

// WorkThread.h
#ifndef WORKTHREAD_H
#define WORKTHREAD_H
#include <QThread>

class WorkThread : public QThread
{
public:
    WorkThread();

protected:
    virtual void run() override;
};
#endif

// WorkThread.cpp
#include "workthread.h"
#include <QDebug>
#include <QThreadStorage>

WorkThread::WorkThread()
{
}

quint64 g_value1 = 0;
void WorkThread::run()
{
    g_value1 = quint64(currentThreadId());
    qDebug() << g_value1;
}

我们继承 QThread 类并重写 run() 函数,函数中的全局变量 g_value1 由线程 ID 赋值。实例化出两个线程对象并均启动。其结果输出如下:

可以看到两个输出的结果是一样的,线程 thr 对全局变量的修改影响了线程 thr1。造成这个现象的原因也很好理解。“线程隶属于某一个进程,与进程内的其他线程一起共享这片地址空间”。也就是说全局变量属于公共资源,被所有线程所共享,只要一个线程修改了这个全局变量自然就会影响其他线程对该全局变量的访问。

而 QThreadStorage 类为每个线程提供了独立的数据存储功能,即使在线程中用到全局变量,只要存在 QThreadStorage 中,也不会影响到其他线程。。如下图所示:

img

需要注意的是,QThreadStorage 的析构函数并不会删除所储存的数据,只有线程退出才会被删除。

posted @ 2022-07-17 09:52  [BORUTO]  阅读(706)  评论(0编辑  收藏  举报