Qt传统的用户界面应用程序都只有一个线程,一次执行一个操作,如果用户调用一个比较耗时的操作(大批量的I/O操作和高精深的算法等),甚至可能引发休眠的操作,那么用户界面将会冻结而不在被响应,而出现“假死”现象。那么我们就需要使用到线程来解决这个问题
电脑中时会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。
广义上是指正在运行的程序实例,狭义上说指的是程序被加载到内存中执行的后得到的进程。
每个进程都会有4G的内存空间。
一个程序可以有多个进程,每个进程都有唯一的进程ID。
进程想要执行任务就需要依赖线程。换句话说,线程就是进程中的最小执行单位就是线程,并且一个进程中至少有一个线程,或者说线程就是进程的子任务。
一个进程可以同时拥有多个线程,即同时被系统调度的多个执行路线.
一个进程的所有线程都共享进程的代码区、数据区、堆区、环境变量和命令行参数、文件描述符、信号处理函数等等
一个进程的每个线程都拥有独立的线程ID。
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1)线程是进程的一个实体,可作为系统独立调度和分派的基本单位。
2)线程有不同的状态,系统提供了多种线程控制原语,如创建、终止、取消、暂停、恢复等等。
3)线程可以使用的大部分资源都是隶属于进程的,即使是在特定线程中动态分配的资源,也同样为进程所拥有。
4)一个进程中可以有多个线程并发地运行。它们可以执行相同的代码,也可以执行不同的代码。
5)同一进程的多个线程都在同一个地址空间内活动,因此相比于进程,线程的系统开销会更小,任务切换更快。
6)进程空间内的代码和数据对于该进程的每个线程而言都是共享的。因此同一个进程的不同线程之间不存在通信问题,当然也就不需要类似IPC的通信机制。
7)线程之间虽然不存在通信问题但是存在冲突问题。同样是因为数据共享,当一个进程的多个线程“同时”访问一份数据时,线程间的冲突可能导致逻辑甚至系统错误。
8)线程之间存在优先级的差异。即使低优先级线程的时间片尚未耗尽,只要有高优先级线程处于就绪状态,就会立即抢夺低优先级线程手中的处理机。
串行:单条线程按照一定的顺序来执行多个任务,比如下载文件,先下载A文件,在下载B文件,最后下载C文件。
并行:同一时刻多条线程同时执行,每个线程都有自己的任务去完成。类似于三个线程A,B,C。A线程下载A文件,B线程下载B文件,C线程下载C文件。
临界资源:多个线程共享同一个进程的资源,而每一次只能有一个线程去访问的资源被称为临界资源。他可以使硬件设备,也可以是一个数据,文件。
临界区:访问临界资源的代码被称为临界区(代码段实施对临界资源的操作)。一次只能有一个线程进入临界区
线程互斥:互斥是指临界资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
线程同步:在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
互斥和同步的关系:
所谓互斥,就是不同线程通过竞争进入临界区(共享的数据和硬件资源),为了防止访问冲突,在有限的时间内只允许其中之一独占性的使用共享资源。如不允许同时写
同步关系则是多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务。一般来说,同步关系中往往包含互斥,同时对临界区的资源会按照某种逻辑顺序进行访问。如先生产后使用
而Qt也提供了对于线程的支持,每个程序启动后拥有的第一个线程称为主线程,即GUI线程。图形用户界面运行于它自己的GUI线程中,而另外的事件处理或操作则会在其它线程中运行,这样即使处理那些数据密集的事件时,也能保证应用程序的图形界面始终保持响应。
QT提供了独立于平台的线程类。QThread 类,它是所有QT线程类的基础,该类提供了很多低级的 API 对线程进行操作。
调用后会执行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 ; //判断线程是否正在运行一个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:启动线程打印数字
1、Qt多线程应用程序的结果具有不确定性,当多次执行同一程序时,每次的运 行结果有可能不同
2、Qt多线程的执行顺序具有不确定性,它与底层操作系统的调度策略和线程优 先级等因素有关
3、Qt多线程的切换时机具有不确定性,可能发生在任何时刻、任何地点,因此 对代码的细微修改都可能产生意想不到的结果
4、基于以上特点,为了避免多线程引发的问题,开发人员必须充分考虑多个线程同步运行时各种可能的结果,必要时可通过互斥锁、信号量等机制加以控制,确保代码的安全性。
如下代码?
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 条件等待
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(); }在较复杂的函数和异常处理中对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是对临界区进行加锁,加锁之后其他线程不可访问,但是有些时候,我们只是在特殊的时候对临界资源的一部分权限进行加锁,比如当我们对数据进行增删改时进行加锁,但是查,看时不受影响时需要怎么做呢?
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(); }在较复杂的函数和异常处理中对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(); }