Qt 多线程详解 - 两种线程使用方式

Qt提供QThread类以进行多任务处理。与多任务处理一样,Qt提供的线程可以做到单个线程做不到的事情。例如,网络应用程序中,可以使用线程处理多种连接器。

QThread继承自QObject类,且提供QMutex类以实现同步。线程和进程共享全局变量,可以使用互斥体对改变后的全局变量值实现同步。因此,必须编辑全局数据时,使用互斥体实现同步,其它进程则不能改变或浏览全局变量值。

什么是互斥体?

互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex))。互斥体禁止多个线程同时进入受保护的代码“临界区”(critical section)。

在任意时刻,只有一个线程被允许进入代码保护区。任何线程在进入临界区之前,必须获取(acquire)与此区域相关联的互斥体的所有权。如果已有另一线程拥有了临界区的互斥体,其他线程就不能再进入其中。这些线程必须等待,直到当前的属主线程释放(release)该互斥体。

什么时候需要使用互斥体呢?

互斥体用于保护共享的易变代码,也就是,全局或静态数据。这样的数据必须通过互斥体进行保护,以防止它们在多个线程同时访问时损坏。

Qt线程基础
QThread的创建和启动

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

如上述代码所示,如果要创建线程,则必须继承QThread类。MyThread使用成员函数run()才会实现线程。

Qt提供的线程类

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

同步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()可以唤醒该线程。

可重入性与线程安全

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

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

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


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

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

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

常见的不可重入函数有:

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

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

QThread和QObjects

QThread类继承自QObjects类。因此,线程开始或结束时,QThread类发生发送信号事件。信号与槽的功能是QThread类从QObject类继承的,可以通过信号与槽处理开始或结束等操作,所以可以实现多线程。QObject是基于QTimer、QTcpSocket、QUdpSocket和QProcess之类的非图形用户界面的子类。

基于非图形用户界面的子类可以无线程操作。单一类运行某功能时,可以不需要线程。但是,运行单一类的目标程序的上级功能时,则必须通过线程实现。

线程A和线程B没有结束的情况下,应设计使主线程时间循环不结束;而若线程A迟迟不结束而导致主线程循环也迟迟不能结束,故也要防止线程A没有在一定时间内结束。


处理QThread的信号和槽的类型

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

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

使用QtConcurrent类的并行编程

QtConcurrent类提供多线程功能,不使用互斥体、读写锁、等待条件和信号量等低级线程。使用QtConcurrent创建的程序会根据进程数自行调整使用的线程数。

 

QThread类

简述

QThread类提供了与系统无关的线程。

QThread代表在程序中一个单独的线程控制。线程在run()中开始执行,默认情况下,run()通过调用exec()启动事件循环并在线程里运行一个Qt的事件循环。

详细描述

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子类的类名)。

线程管理

可以将常用的接口按照功能进行以下分类:

线程启动

void start(Priority priority = InheritPriority) [slot] 

调用后会执行run()函数,但在run()函数执行前会发射信号started(),操作系统将根据优先级参数调度线程。如果线程已经在运行,那么这个函数什么也不做。优先级参数的效果取决于操作系统的调度策略。特别是那些不支持线程优先级的系统优先级将会被忽略(例如在Linux中,更多细节请参考http://linux.die.net/man/2/sched_setscheduler)。
 

线程执行

int exec() [protected] 

进入事件循环并等待直到调用exit(),返回值是通过调用exit()来获得,如果调用成功则范围0。

 

void run() [virtual protected] 

线程的起点,在调用start()之后,新创建的线程就会调用这个函数,默认实现调用exec(),大多数需要重新实现这个函数,便于管理自己的线程。该方法返回时,该线程的执行将结束。

 

线程退出

void quit() [slot] 

告诉线程事件循环退出,返回0表示成功,相当于调用了QThread::exit(0)。

 

void exit(int returnCode = 0) 

告诉线程事件循环退出。 调用这个函数后,线程离开事件循环后返回,QEventLoop::exec()返回returnCode,按照惯例,0表示成功;任何非0值表示失败。

 

void terminate() [slot] 

终止线程,线程可能会立即被终止也可能不会,这取决于操作系统的调度策略,使用terminate()之后再使用QThread::wait(),以确保万无一失。当线程被终止后,所有等待中的线程将会被唤醒。 

警告:此函数比较危险,不鼓励使用。线程可以在代码执行的任何点被终止。线程可能在更新数据时被终止,从而没有机会来清理自己,解锁等等。。。总之,只有在绝对必要时使用此函数。
 

 

void requestInterruption() 

请求线程的中断。该请求是咨询意见并且取决于线程上运行的代码,来决定是否及如何执行这样的请求。此函数不停止线程上运行的任何事件循环,并且在任何情况下都不会终止它。

线程等待

void msleep(unsigned long msecs) [static]     //强制当前线程睡眠msecs毫秒
 
void sleep(unsigned long secs) [static]     //强制当前线程睡眠secs秒
 
void usleep(unsigned long usecs) [static]     //强制当前线程睡眠usecs微秒
 
bool wait(unsigned long time = ULONG_MAX)     //线程将会被阻塞,等待time毫秒。和sleep不同的是,如果线程退出,wait会返回。

线程优先级

void setPriority(Priority priority) 

设置正在运行线程的优先级。如果线程没有运行,此函数不执行任何操作并立即返回。使用的start()来启动一个线程具有特定的优先级。优先级参数可以是QThread::Priority枚举除InheritPriortyd的任何值。

Qt多线程优先级
常量 优先级
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 使用和创建线程同样的优先级. 这是默认值

 

QThread类使用方式

QThread的使用方法有如下两种:

  • QObject::moveToThread()
  • 继承QThread类

QObject::moveToThread

方法描述:

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

例子:

#ifndef WORKER_H
#define WORKER_H
#include 
#include
#include
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:"<
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include 
#include
#include
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:"<
#include "controller.h"
#include 
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类

方法描述

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

例子:

#ifndef MYTHREAD_H
#define MYTHREAD_H
#include
#include
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:"<
#include "controller.h"
#include 
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();
}

两种方法的比较

两种方法来执行线程都可以,随便你的喜欢。不过看起来第二种更加简单,容易让人理解。不过我们的兴趣在于这两种使用方法到底有什么区别?其最大的区别在于:

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

QThread的信号与槽

启动或终止线程时,QThread提供了信号与槽。

QThread的信号
信号 含义
void finished() 终止线程实例运行,发送信号
void started() 启动线程实例,发送信号
void terminated() 结束线程实例,则发送信号
QThread的槽
含义
void quit() 线程终止运行槽
void start(Priority) 线程启动槽
void terminate() 线程结束槽

 

你可能感兴趣的:(Qt开发,Qt,多线程详解,Qt,多线程,Qt,多线程的两种方式)