本文为QT,C++开发知识点整理。
多线程
QThread创建多线程
一个QThread类的对象管理一个线程,一般从QThread继承一个自定义类,并重定义虚函数run(),在run()函数里实现线程所需要的任务。
将应用程序的线程称为主线程,额外创建的线程则称之为工作线程,一般在主线程里创建工作线程,并调用start()开始执行工作线程的任务,start()会在内部调用run()函数,在run()函数里调用exit()或quit()可以结束线程的事件循环,或在主线程里调用terminate()强制结束线程。
myQthread类是我们自己定义的类,我们需要对run()函数进行重写。
1 | void run() Q_DECL_OVERRIDE; |
Q_DECL_OVERRIDE相当于C++的override,加上以便区分。
通过这样继承之后,我们在调用时,有
1 | myQthread a; |
这样通过start()函数来进行调用run()函数。
类型 | 函数 | 功能 |
---|---|---|
公共函数 | bool isFinished() | 线程是否结束 |
bool isRunning() | 线程是否正在运行 | |
Priority priority() | 返回线程的优先级 | |
void exit(int retrunCode=0) | 退出线程,return Code为0表成功,否则有错误 | |
void setPriority(Priority priority) | 设置线程优先级 | |
bool wait(unsigned long time) | 阻止线程执行,直到线程结束。或等待时间超过time毫秒 | |
公共槽函数 | void quit() | 退出线程,返回代码0 |
void start(Priority priority) | 内部调用run()开始执行线程,操作系统根据priority进行调度终止线程的运行 | |
信号 | void finished() | 在线程要结束时发射此信号 |
void start() | 在线程开始,run()函数被调用之前发射此信号 | |
静态公共成员 | int idealThreadCount() | 返回系统上能运行的线程的理想个数 |
void msleep(unsigned long msecs) | 强制当前线程休眠msecs毫秒 | |
void sleep(unsigned long secs) | 强制当前线程休眠secs秒 | |
void usleep(unsigend long usecs) | 强制当前线程休眠usecs微秒 | |
保护函数 | virtual void run() |
基于互斥量的线程同步
QMutex和QMutexLocker是基于互斥量的线程同步类,QMutex定义的实例是一个互斥量,QMutex主要提供以下三个函数:
- lock():锁定互斥量,如果另外一个线程锁定了这个互斥量,它将阻塞执行直到其它线程解锁这个互斥量。
- unlock():解锁一个互斥量,需要与lock()配对使用。
- tryLock():试图锁定一个互斥量,如果成功锁定就返回true,如果其他线程已经锁定了这个互斥量,就返回false,但不阻塞程序执行。
1 | class QDiceThread: public Qthread |
在run函数中,对重新计算筛子点数和掷筛子次数的3行代码用互斥量mutex的lock()和unlock()进行了保护,这部分代码的执行不会被其他线程中断。
注:lock()和unlock()必须配对使用
在readValue()函数中,用互斥量Mutex的tryLock()和unlock()进行了保护。如果tryLock()成功锁定互斥量,读取数值的两行代码执行时不会被中断,执行完后解锁,如果tryLock()锁定失败,函数就立即返回,而不会等待。
QMutexLocker是另外一个简化了互斥量处理的类。QMutexLocker的构造函数接受一个互斥量作为参数并将其锁定,QMutexLocker的析构函数则将此互斥量解锁,所以在QMutexLocker实例变量的生存期内的代码段得到保护,自动进行互斥量的锁定和解锁。
1 | void QDiceThread::run(){ |
这样定义的QDiceThread类,在主程序中只能调用其readValue()函数来不断读取数值。
基于QReadWriteLock的线程同步
使用互斥量时存在一个问题:每次只能有一个线程获得互斥量的权限。如果在一个程序中有多个线程读取某个变量,使用互斥量时也必须排除。而实际上若只是读取一个变量,是可以让多个线程同时访问的,这样互斥量会降低程序的性能。
假设有一个数据采集程序,一个线程负责采集数据到缓冲区,一个线程负责读取缓冲区的数据并显示,另一个线程负责读取缓冲区的数据并保存文件,示意代码如下 :
1 | int buffer[100]; |
数据缓冲区buffer和互斥量mutex都是全局变量,线程threadDAO将数据写到buffer,线程threadShow和threadSaveFile只是读取buffer,但是因为使用互斥量,这3个线程任何时候都只能有一个线程可以访问buffer。实际上,threadShow和threadSaveFile都只是读取buffer的数据,它们同时访问buffer是不会发生冲突。
Qt提供了QReadWriteLock类,它是基于读或写的模式进行代码段锁定的,在多个线程读取一个共享数据时,可以解决上面所说的互斥量存在的问题。
QReadWriteLock以读或写锁定的同步方法允许以读或写的方式保护一段代码,它可以允许多个线程以只读的方式同步访问资源,但是只要有一个线程在以写方式访问资源时,其他线程就必须等待直到写操作结束。
QReadWriteLock提供以下几个主要的函数:
- lockForRead(),以只读方式锁定资源,如果有其他线程以写入方式锁定,这个函数阻塞。
- lockForWrite(),以写入方式锁定资源,如果本线程或其他线程以读或写模式锁定资源,这个函数就阻塞。
- unlock(),解锁。
- tryLockForRead(),是lockForRead()的非阻塞版本。
- tryLockForWrite(),是lockForWrite()的非阻塞版本。
使用QReadWriteLock,上面的三线程代码可以必定成如下形式:
1 | int buffer[100]; |
如果threadDAQ没有以lockForWrite()锁定Lock,threadShow和threadSaveFile可以同时访问buffer,否则threadShow和threadSaveFile都被阻塞;如果threadShow和threadSaveFile都没有锁定,那threadDAQ能以写入方式锁定,否则threadDAQ就被阻塞。
QReadLocker和QWriteLocker是QReadWriteLock的简便形式,如同QMutexLocker是QMutex的简便版本一样,无需与unlock()配对使用。使用QReadLocker和QWriteLocker,则上面的代码可以改写为:
1 | int buffer[100]; |
基于QWaitCondition的线程同步
QWaitCondition提供另外一种改进的线程同步方法。QWaitCondition与QMutex结合,可以使一个线程在满足一定条件时通知其他多个线程,使它们及时作出响应,这样比只使用互斥量效率要高一些。例如,threadDAQ在写满一个缓冲区之后,及时通知threadShow和threadSaveFile,使它们可以及时读取缓冲区数据。
QWaitCondition提供如下一些函数:
- wait(QMutex*lockedMutex),解锁互斥量lockedMutex,并阻塞等待唤醒条件,被唤醒后锁定lockedMutex并退出函数。
- wakeAll(),唤醒所有处于等待状态的线程,线程唤醒的顺序不确定,由操作系统的调度策略决定。
- wakeOne(),唤醒一个处于等待状态的线程,唤醒哪个线程不确定,由操作系统的调度策略决定。
线程类QThreadProducer专门负责掷筛子产生点数,线程类QThreadConsumer专门及时读取数据,并送给主线程进行显示。
1 | QMutex mutex; |
QThreadProducer::run()函数负责每隔500毫秒掷筛子产生一次数据,新数据产生后通过等待条件唤醒所有等待的线程,即:
1 | newdataAvailable.wakeAll(); |
QThreadConsumer::run()函数中的while循环,首先要将互斥量锁定,再执行下面的一条语句:
1 | newdataAvailable.wait(&mutex); |
这条语句以mutex作为输入参数,内部会首先解锁mutex,使其它线程可以使用mutex,newdataAvailable进行等待状态。当QThreadProducer产生新数据使用newdataAvailable.wakAll()唤醒所有线程后,newdataAvailable.wait(&mutex)会再次锁定mutex,然后退出阻塞状态,以执行后面的语句。
基于信号量的线程同步
信号量是另一种限制对共享资源进行访问的线程同步机制。一个互斥量只能被锁定一次,而信号量可以多次使用。信号量通常用来保护一定数量的相同的资源,如数据采集时的双缓冲区。
QSemaphore是实现信号量功能的类,它可以提供以下几个基本的函数。
acquire(int n)尝试获得n个资源。如果没有这么多资源,线程将阻塞直到有n个资源可用。
release(int n)释放n个资源,如果信号量的资源已全部可用之后再release(),就可以创建更多的资源,增加可用资源的个数。
int available()返回当前信号量可用的资源个数,这个数永远不可能为负数,如果为0,就说明当前没有资源可用。
bool tryAcquire(int n=1),尝试获取n个资源,不成功时不阻塞线程。
1 | QSemaphore wc(5); //WC.available()==5,初始化资源为5 |
双缓冲区数据采集和读取线程类设计
信号量通常用来保护一定数量的相同的资源,如数据采集时的双缓冲区,适用于Producer/Consumer模型。
1 | const int BufferSize=8; |
信号量emptyBufs初始资源个数为2,表示有2个空的缓冲区可用。
信号量fullBufs初始资源个数为0,表示写满数据的缓冲区个数为零。