QT多线程

tech2022-08-18  73

一、概念

Qt传统的用户界面应用程序都只有一个线程,一次执行一个操作,如果用户调用一个比较耗时的操作(大批量的I/O操作和高精深的算法等),甚至可能引发休眠的操作,那么用户界面将会冻结而不在被响应,而出现“假死”现象。那么我们就需要使用到线程来解决这个问题

1、什么是进程?

电脑中时会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。

广义上是指正在运行的程序实例,狭义上说指的是程序被加载到内存中执行的后得到的进程。

每个进程都会有4G的内存空间。

一个程序可以有多个进程,每个进程都有唯一的进程ID。

2、什么是线程?

进程想要执行任务就需要依赖线程。换句话说,线程就是进程中的最小执行单位就是线程,并且一个进程中至少有一个线程,或者说线程就是进程的子任务。

一个进程可以同时拥有多个线程,即同时被系统调度的多个执行路线.

一个进程的所有线程都共享进程的代码区、数据区、堆区、环境变量和命令行参数、文件描述符、信号处理函数等等

一个进程的每个线程都拥有独立的线程ID。

3、线程和进程的区别?

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

4、线程的基本特点

1)线程是进程的一个实体,可作为系统独立调度和分派的基本单位。

2)线程有不同的状态,系统提供了多种线程控制原语,如创建、终止、取消、暂停、恢复等等。

3)线程可以使用的大部分资源都是隶属于进程的,即使是在特定线程中动态分配的资源,也同样为进程所拥有。

4)一个进程中可以有多个线程并发地运行。它们可以执行相同的代码,也可以执行不同的代码。

5)同一进程的多个线程都在同一个地址空间内活动,因此相比于进程,线程的系统开销会更小,任务切换更快。

6)进程空间内的代码和数据对于该进程的每个线程而言都是共享的。因此同一个进程的不同线程之间不存在通信问题,当然也就不需要类似IPC的通信机制。

7)线程之间虽然不存在通信问题但是存在冲突问题。同样是因为数据共享,当一个进程的多个线程“同时”访问一份数据时,线程间的冲突可能导致逻辑甚至系统错误。

8)线程之间存在优先级的差异。即使低优先级线程的时间片尚未耗尽,只要有高优先级线程处于就绪状态,就会立即抢夺低优先级线程手中的处理机。

5、线程相关概念

串行:单条线程按照一定的顺序来执行多个任务,比如下载文件,先下载A文件,在下载B文件,最后下载C文件。

并行:同一时刻多条线程同时执行,每个线程都有自己的任务去完成。类似于三个线程A,B,C。A线程下载A文件,B线程下载B文件,C线程下载C文件。

临界资源:多个线程共享同一个进程的资源,而每一次只能有一个线程去访问的资源被称为临界资源。他可以使硬件设备,也可以是一个数据,文件。

临界区:访问临界资源的代码被称为临界区(代码段实施对临界资源的操作)。一次只能有一个线程进入临界区

线程互斥:互斥是指临界资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

线程同步:在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。

互斥和同步的关系:

所谓互斥,就是不同线程通过竞争进入临界区(共享的数据和硬件资源),为了防止访问冲突,在有限的时间内只允许其中之一独占性的使用共享资源。如不允许同时写

同步关系则是多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务。一般来说,同步关系中往往包含互斥,同时对临界区的资源会按照某种逻辑顺序进行访问。如先生产后使用

二、QT中的线程

而Qt也提供了对于线程的支持,每个程序启动后拥有的第一个线程称为主线程,即GUI线程。图形用户界面运行于它自己的GUI线程中,而另外的事件处理或操作则会在其它线程中运行,这样即使处理那些数据密集的事件时,也能保证应用程序的图形界面始终保持响应。

QT提供了独立于平台的线程类。QThread 类,它是所有QT线程类的基础,该类提供了很多低级的 API 对线程进行操作。

1、相关的函数

void start(Priority priority = InheritPriority);//启动线程函数

调用后会执行run()函数,但在run()函数执行前会发射信号started(),操作系统将根据优先级参数调度线程。如果线程已经在运行,那么这个函数什么也不做。优先级参数的效果取决于操作系统的调度策略。

int exec();//线程事件循环

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

virtual void run();//线程功能代码实现函数

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

void quit();//退出线程

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

void exit(int returnCode = 0);//退出循环

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

void terminate();//终止循环

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

当线程被终止后,所有等待中的线程将会被唤醒。

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

建议:一般情况下,都在run函数里面设置一个标识符,可以控制循环停止。然后才调用quit函数,退出线程。

void msleep(unsigned long msecs);//强制当前线程睡眠msecs毫秒 void sleep(unsigned long secs);//强制当前线程睡眠secs秒 void usleep(unsigned long usecs);//强制当前线程睡眠usecs微秒 bool wait(unsigned long time = ULONG_MAX);//等待线程结束

线程将会被阻塞,等待time毫秒。和sleep不同的是,如果线程退出,wait会返回。

bool isFinished() const;   //判断线程是否结束 bool isRunning() const  ;  //判断线程是否正在运行

2、创建线程方法

一个QThread对象管理一个线程,QThread的执行从run()函数的执行开始,在Qt自带的QThread类中,run()函数通过调用exec()函数来启动事件循环机制,并且在线程内部处理Qt的事件。在Qt中建立线程的主要目的就是为了用线程来处理那些耗时的后台操作,从而让主界面能及时响应用户的请求操作。使用该类开新线程并运行某段代码的方式一般有两种:

创建方法一:子类化QThread

创建一个子类继承于QThread,重写QThread::run()函数,将需要子线程所完成的代码写入到这个子线程中即可,将来通过调用QThread::start(),即可开启子线程,让run函数里面在子线程中执行,如下所示:

class myThread:public Qthread{

protected:

      virtual void run(void){

                  //此函数在子线程中运行

      }

};

课堂案列1:启动线程打印数字

注意:重写run函数时,如果是需要事件循环,信号和槽技术的语句,比如访问网络之类的,那么每次都需要调用exec()来进入到事件循环,不加的话槽函数不会被触发,如果只是处理一些耗时的循环,比如一个100w次的循环,然后把结果传出线程,那么就不用调用exec()函数。建议在写此类时,设置标志,加上一个开始函数和结束函数,在开始函数将标志设置为true,结束函数时设置为false。

创建方法二:子类化QObject

在Qt中,也可以通过QObject::moveToThread可以将工作对象移动到子线程中,这时通过信号可以触发工作对象槽函数,该槽函数将会在子线程的事件循环中被响应和执行

相比方法一,使用moveToThread可以确保工作对象任意槽函数代码在子线程中被执行,也可以使用任意信号触发该槽的执行,只要确保信号和槽参数可以匹配即可。

使用moveToThread必须满足以下条件:

工作对象继承自QObject或者其子类

工作对象不能有父对象(parent)

如下所示:

class SimpleThreadTwo : public QObject{

    Q_OBJECT

public slots:

    void doSomething(){

                   子线程的工作代码

     }

};

在主线程的对象构造中写如下代码

QThread* m_objThread= new QThread();

SimpleThreadTwo* m_ThreadTwo= new SimpleThreadTwo();

m_ThreadTwo->moveToThread(m_objThread);//将线程一如到QThread中

connect(m_objThread,SIGNAL(finished),m_objThread,SLOT(deleteLater));//线程结束时销毁

connect(putton,SIGNAL(clicked()),m_ThreadTwo,SLOT(doSomething));//点击按钮开启线程

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

课堂案列2:启动线程打印数字

3、QT线程的特点:

1、Qt多线程应用程序的结果具有不确定性,当多次执行同一程序时,每次的运 行结果有可能不同

2、Qt多线程的执行顺序具有不确定性,它与底层操作系统的调度策略和线程优 先级等因素有关

3、Qt多线程的切换时机具有不确定性,可能发生在任何时刻、任何地点,因此 对代码的细微修改都可能产生意想不到的结果

4、基于以上特点,为了避免多线程引发的问题,开发人员必须充分考虑多个线程同步运行时各种可能的结果,必要时可通过互斥锁、信号量等机制加以控制,确保代码的安全性。

三、线程同步

1、线程同步的问题:

如下代码?

int g_data=0; void SetData(){ g_data++; qDebug()<<g_data; } void Thread1 :: run (){ SetData(); } void Thread2 :: run (){ SetData(); }

这是实现从0开始递增且不允许重复的值的代码。

但是这样写是不安全的,当我们两个线程同时修改全局变量g_data时,就会出现相互覆盖,即thread1刚修改了g_data的值还没有打印时,thread2就访问了临界区,将thread1的结果给改变了,所以很有可能两个线程会打印出同一个数字。

在这个代码中,实际上g_data就是一个临界区资源。

线程同步是指多个线程同时访问临界区资源时,需要一个线程访问结束其它线程才能访问,那么对于多线程的Qt应用程序,最基本的要求就是能够实现多个线程同时运行, 并且要确保不会因为抢占资源而引发问题。  

线程锁能够保证临界资源的安全性,通常,每个临界资源需要一个线程锁进行保护。

线程死锁:线程间相互等待临界资源而造成彼此无法继续执行。

产生死锁的条件:

    A、系统中存在多个临界资源且临界资源不可抢占

    B、线程需要多个临界资源才能继续执行

死锁的避免:

    A、对使用的每个临界资源都分配一个唯一的序号

    B、对每个临界资源对应的线程锁分配相应的序号

    C、系统中的每个线程按照严格递增的次序请求临界资源

Qt提供了以下几种机制用于线程同步

QMutex 互斥锁(互斥量)

QReadWriteLock 读写锁

QSemaphore 信号量

QWaitCondition 条件等待

2、互斥量-QMutex

QMutex 提供相互排斥的锁,或互斥量。QMutex常用来保护一段临界区代码。即每次只能有一个线程访问这段代码,QMutex类所以成员函数是线程安全的。

QMutex类的lock()函数用于锁住mutex。也就是说当调用lock()函数,那么线程就会立即抓住并锁住它,其他线程访问时,就会被阻塞,进入休眠状态,直到该线程调用unlock()函数进行解锁时,其他线程才能进入。线程调用lock()函数后就会持有这个互斥量,指导调用unlock结束时为止。

QMutex类还提供了tryLock()函数,当互斥量是锁住状态,其他线程访问时,并不会阻塞,进入休眠状态,而是理解结束。

int g_data=0; QMutex mutex; void SetData(){ mutex.lock(); g_data++; qDebug()<<g_data; mutex.unlock(); } void Thread1 :: run (){ SetData(); } void Thread2 :: run (){ SetData(); }

3、互斥锁QMutexLocker

在较复杂的函数和异常处理中对QMutex类mutex对象进行lock()和unlock()操作将会很复杂,进入点要lock(),在所有跳出点都要unlock(),很容易出现在某些跳出点未调用unlock(),所以Qt引进了QMutex的辅助类QMutexLocker来避免lock()和unlock()操作。在函数需要的地方建立QMutexLocker对象,并把mutex指针传给QMutexLocker对象,此时mutex已经加锁,等到退出函数后,QMutexLocker对象局部变量会自己销毁,此时mutex解锁。

int g_data=0; QMutex mutex; void SetData(){ QMutexLocker locker(&mutex); g_data++; qDebug()<<g_data; } void Thread1 :: run (){ SetData(); } void Thread2 :: run (){ SetData(); }

上述代码中locker对象时局部变量,当SetData()函数结束时,locker对象就会被自动释放,也就自动对互斥量mutex解锁。

QMutex是对临界区进行加锁,加锁之后其他线程不可访问,但是有些时候,我们只是在特殊的时候对临界资源的一部分权限进行加锁,比如当我们对数据进行增删改时进行加锁,但是查,看时不受影响时需要怎么做呢?

4、读写锁QReadWriteLock

QReadWriteLock(读写锁)用于线程同步,可以允许多个线程同时读取共享资源,但是同时只能有一个线程修改共享资源。

QReadWriterLock 与QMutex相似,但对读写操作访问进行区别对待,可以允许多个读者同时读数据,但只能有一个写,并且写读操作不能同时进行。使用QReadWriteLock而不是QMutex,可以使得多线程程序更具有并发性,提供程序的性能。

QReadWriteLock lock; void Read_File(){ QFile file("./stu.bin"); qDebug()<<file.readAll(); } void Write_File(){ QFile file("./stu.bin"); file.write("hello",6); } void ReaderThread::run(){ lock.lockForRead(); read_file(); lock.unlock(); } void WriterThread::run(){ lock.lockForWrite(); write_file(); lock.unlock(); }

5、QReadLocker和QWriteLocker

 在较复杂的函数和异常处理中对QReadWriterLock类lock对象进行lockForRead()/lockForWrite()和unlock()操作将会很复杂,进入点要lockForRead()/lockForWrite(),在所有跳出点都要unlock(),很容易出现在某些跳出点未调用unlock(),所以Qt引进了QReadLocker和QWriteLocker类来简化解锁操作。在函数需要的地方建立QReadLocker或QWriteLocker对象,并把lock指针传给QReadLocker或QWriteLocker对象,此时lock已经加锁,等到退出函数后,QReadLocker或QWriteLocker对象局部变量会自己销毁,此时lock解锁。

QReadWriteLock lock; void Read_File(){ QReadLocker Readlocker(&lock);//加读锁 QFile file("./stu.bin"); qDebug()<<file.readAll(); } void Write_File(){ QWriteLocker WriteLocker(&lock);//加写锁 QFile file("./stu.bin"); file.write("hello",6); } void ReaderThread::run(){ read_file(); } void WriterThread::run(){ write_file(); }

6、信号量-QSemaphore

信号量可以理解为对互斥功能的扩展,互斥量只能锁定一次而信号量可以获取多次,它可以用来保护一定数量的同种资源。举例来说信号量就类似于去海底捞吃火锅的时候,海底捞场地就餐桌数量是固定的,假设有5桌。现在来了8个人,那么其他3个就需要在门口候餐区等待加号。当有其他桌吃完离开之后,进去一个。

Semaphore提供两种基本的操作,acquire() 和release():

acquire(n)尝试去n个资源。如果获取不到足够的资源,这个会一直锁住直到可以获取足够的资源。

Release(n)释放n个资源。

除了以上的方法,还有tryAcquire()函数,如果得不到足够的资源会立即返回,available()函数,返回在任意时刻可用的资源数目。

信号量的典型用例是控制生产者/消费者之间共享的环形缓冲区。

– 生产者线程向全局缓冲区存放数据

– 消费者线程从全局缓冲区获取数据

生产和消费的同步问题

– 如果生产者线程生产的速度太快,那么将会把消费者还未读取的数据覆盖掉

– 如果消费者线程读取数据过快,那么将会越过生产者线程读取到一些垃圾数据

解决该问题的一个简单方法就是让生产者者先填满缓冲区,停止生产;然后让消费者线程读取完缓冲区中的全部数据,停止消费恢复生产,也就是同一 时刻只有一个线程在访问缓冲区数据,这样固然可以避免上述的问题,但在 多CPU的系统上,效率会收到极大影响 •

解决生产者和消费者一个更为有效的方法是通过使用两个信号量:

– 控制生产者信号量:控制缓冲区的空闲位置,有空闲位置才可生产,否则等待 QSemaphore freeSpace(buffSize);

– 控制消费者信号量:控制缓冲区可使用数据字节数,有数据才能读取,否则等待 QSemaphore userSpace(0);

#include <QCoreApplication> #include <QThread> #include <QSemaphore> #define DATASIZE 20//生产的产品总量 #define BUFFERSIZE 5//仓库大小 int buffer[BUFFERSIZE]={0};//仓库 QSemaphore freeSpace(BUFFERSIZE);//控制生产这信号量 QSemaphore userSpace(0);//控制消费者信号量 class threadProducer:public QThread{//生产者线程 void run(){ for(int i=0;i<DATASIZE;i++){ freeSpace.acquire();//对于生产者来说,生产一个,仓库就被占一个所以仓库数-1 buffer[i%BUFFERSIZE]=i+1; qDebug("生产者:%d",buffer[i%BUFFERSIZE]); //生产完成,多一个可以消费产品,控制消费信号量+1; userSpace.release();//对于消费者来说,仓库每生产一个,就可以进行消费,随意消费多了一个 +1 msleep(200);//模拟生产过程 } } }; class threadConsumer:public QThread{//消费者线程 void run(){ for(int i=0;i<DATASIZE;i++){ userSpace.acquire();//对于消费者,我要进行消费,所以消费产品-1 qDebug("消费者:%d",buffer[i%BUFFERSIZE]); freeSpace.release();//消费者消费一个产品,仓库就会有一个空闲,所以仓库就+1, msleep(400);//模拟消费 } } }; int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); threadProducer producer;//生产 threadConsumer consumer;//消费 producer.start();//生产者开始生产 consumer.start();//消费者开始消费 return a.exec(); }

7、等待条件QWaitCondition

  QWaitConditon也是用来同步线程的。从名字来看是等待条件,意思就是线程阻塞在等待条件的地方,直到条件满足才继续执行下去。等待条件的线程可以是一个或者多个。用QWaitCondition的函数表示过程如下:

1.等待条件的线程调用QWaitCondition::wait()阻塞。

2.实现条件的线程通过计算完成条件后调用QWaitConditon::wakeOne()或者QWaitCondition::wakeAll()来唤醒线程。

3.当2中调用wake之后,继续执行wait之后的操作。

其中wakeOne会随机唤醒等待的线程中的一个。wakeAll会唤醒所有的等待线程。

对于生产者和消费者的同步问题,也可以使用等待条件(QWaitCondition), 它可以让线程阻塞等待,并允许另一个线程在满足一定条件下触发等待的线程继续执行

– 在生产者线程中,首先检查缓冲区是否写满,如果没有写满就向缓冲区写 入数据,并且同时触发所有等待“缓冲区非空”的条件 的消费者线程,如果写满则等待“缓冲区没有写满”这个条件”

– 消费者线程所做的和生产者正好相反,它等待“缓冲区非空”的条件并且 触发正在等待“缓冲区没有写满”条件的任意线程

#include <QCoreApplication> #include <QThread> #include <QWaitCondition> #include <QMutex> #include <QDebug> #define DATASIZE 20//生产的产品总量 #define BUFFERSIZE 5//仓库大小 int buffer[BUFFERSIZE]={0};//仓库 int used = 0;//仓库的数量 QWaitCondition freeSpace;//控制生成者 QWaitCondition userSpace;//控制消费者 QMutex tex;//锁 class threadProducer:public QThread{     void run(){         for(int i=0;i<DATASIZE;i++){             tex.lock();             if(used>=BUFFERSIZE){//当前生成了五个,不能在生成                 freeSpace.wait(&tex);//生产者线程阻塞             }             tex.unlock();             buffer[i%BUFFERSIZE]=i;//生产一个数据记录到仓库中             qDebug("生产者:%d",buffer[i%BUFFERSIZE]);             tex.lock();             used++;//产品增加1             userSpace.wakeAll();//让消费者可以去消费了             tex.unlock();         }     } }; class threadConsumer:public QThread{     void run(){         for(int i= 0;i<DATASIZE;i++){             tex.lock();             if(used<=0){//当仓库中的数量为0时,没有产品可供消费                 userSpace.wait(&tex);//消费者线程阻塞             }             tex.unlock();             qDebug("消费者:%d",buffer[i%BUFFERSIZE]);//消费者进行消费             tex.lock();             used--;//消费者消费一个,仓库多了一个空间             freeSpace.wakeAll();//让生产者取消阻塞,进行生产             tex.unlock();         }     } }; int main(int argc, char *argv[]) {     QCoreApplication a(argc, argv);     threadProducer Producer;     threadConsumer Consumer;     Producer.start();     Consumer.start();     return a.exec(); }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

最新回复(0)