0%

Qt5.9-开发指南

本文为QT,C++开发知识点整理。

多线程

QThread创建多线程

一个QThread类的对象管理一个线程,一般从QThread继承一个自定义类,并重定义虚函数run(),在run()函数里实现线程所需要的任务。

将应用程序的线程称为主线程,额外创建的线程则称之为工作线程,一般在主线程里创建工作线程,并调用start()开始执行工作线程的任务,start()会在内部调用run()函数,在run()函数里调用exit()或quit()可以结束线程的事件循环,或在主线程里调用terminate()强制结束线程。

绘图1

myQthread类是我们自己定义的类,我们需要对run()函数进行重写。

1
void run() Q_DECL_OVERRIDE;

Q_DECL_OVERRIDE相当于C++的override,加上以便区分。

通过这样继承之后,我们在调用时,有

1
2
myQthread a;
a.start();

这样通过start()函数来进行调用run()函数。

Qthread类的主要接口
类型 函数 功能
公共函数 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主要提供以下三个函数:

  1. lock():锁定互斥量,如果另外一个线程锁定了这个互斥量,它将阻塞执行直到其它线程解锁这个互斥量。
  2. unlock():解锁一个互斥量,需要与lock()配对使用。
  3. tryLock():试图锁定一个互斥量,如果成功锁定就返回true,如果其他线程已经锁定了这个互斥量,就返回false,但不阻塞程序执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class QDiceThread: public Qthread
{
Q_OBJECT

private:

QMutex mutex;//互斥量
int m_seq=0;//序号
int m_diceValue;

protected:

void run() Q_DECL_OVERRIDE;

public:

bool readValue(int*seq,int*diceValue);//用于主线程读取数据的函数
}


void QDiceThread::run(){

m_stop=false;
m_seq=0;
qsrand(QTime::currentTime().msec());//随机数初始化
while(!m_stop) //循环主体
{
if(!m_paused){

mutex.lock();
m_diceValue=qrand();
m_diceValue=(m_diceValue%6)+1;
m_seq++;
mutex.unlock();

}
msleep(500);//结束休眠
}

}

bool QDiceThread::readValue(int *seq,int*diceValue){

if(mutex.tryLock()){

*seq=m_seq;
*diceValue=m_diceValue;
mutex.unlock();
return true;
}
return false;
}

在run函数中,对重新计算筛子点数和掷筛子次数的3行代码用互斥量mutex的lock()和unlock()进行了保护,这部分代码的执行不会被其他线程中断。

:lock()和unlock()必须配对使用

在readValue()函数中,用互斥量Mutex的tryLock()和unlock()进行了保护。如果tryLock()成功锁定互斥量,读取数值的两行代码执行时不会被中断,执行完后解锁,如果tryLock()锁定失败,函数就立即返回,而不会等待。

QMutexLocker是另外一个简化了互斥量处理的类。QMutexLocker的构造函数接受一个互斥量作为参数并将其锁定,QMutexLocker的析构函数则将此互斥量解锁,所以在QMutexLocker实例变量的生存期内的代码段得到保护,自动进行互斥量的锁定和解锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void QDiceThread::run(){

m_stop=false;
m_seq=0;
qsrand(QTime::currentTime().msec());//随机数初始化

while(!m_stop){//循环主体

if(!m_paused){
QMutexLocker locker(&mutex);
m_diceValue=qrand();//获取随机数
m_diceValue=(m_diceValue%6)+1;
m_seq++;
}
msleep(500);//线程休眠
}

}

这样定义的QDiceThread类,在主程序中只能调用其readValue()函数来不断读取数值。

基于QReadWriteLock的线程同步

使用互斥量时存在一个问题:每次只能有一个线程获得互斥量的权限。如果在一个程序中有多个线程读取某个变量,使用互斥量时也必须排除。而实际上若只是读取一个变量,是可以让多个线程同时访问的,这样互斥量会降低程序的性能。

假设有一个数据采集程序,一个线程负责采集数据到缓冲区,一个线程负责读取缓冲区的数据并显示,另一个线程负责读取缓冲区的数据并保存文件,示意代码如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int buffer[100];
QMutex mutex;

void threadDAQ::run(){
...
mutex.lock();
get_data_and_write_in_buffer(); //数据写入buffer
mutex.unlock();
...
}

void threadShow::run(){
...
mutex.lock();
show_buffer(); //读取buffer里的数据并显示
mutex.unlock();
...
}

void threadSaveFile::run(){
...
mutex.lock();
Save_buffer_toFile(); //读取buffer里的数据并保存文件
mutex.unlock();
...
}

数据缓冲区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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int buffer[100];
QReadWriteLock Lock;

void threadDAQ::run(){
...
Lock.lockForWrite();
get_data_and_write_in_buffer(); //数据写入buffer
Lock.unlock();
...
}

void threadShow::run(){
...
Lock.lockForRead();
show_buffer(); //读取buffer里的数据并显示
Lock.unlock();
...
}

void threadSaveFile::run(){
...
Lock.lockForRead();
Save_buffer_toFile(); //读取buffer里的数据并保存文件
Lock.unlock();
...
}

如果threadDAQ没有以lockForWrite()锁定Lock,threadShow和threadSaveFile可以同时访问buffer,否则threadShow和threadSaveFile都被阻塞;如果threadShow和threadSaveFile都没有锁定,那threadDAQ能以写入方式锁定,否则threadDAQ就被阻塞。

QReadLocker和QWriteLocker是QReadWriteLock的简便形式,如同QMutexLocker是QMutex的简便版本一样,无需与unlock()配对使用。使用QReadLocker和QWriteLocker,则上面的代码可以改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int buffer[100];
QReadWriteLock lock;

void threadDAQ::run(){
...
QWriteLocker locker(&lock);
get_data_and_write_in_buffer(); //数据写入buffer
...
}

void threadShow::run(){
...
QReadLocker Locker(&lock);
show_buffer(); //读取buffer里的数据并显示
...
}

void threadSaveFile::run(){
...
QReadLocker locker(&lock);
Save_buffer_toFile(); //读取buffer里的数据并保存到文件
...
}

基于QWaitCondition的线程同步

QWaitCondition提供另外一种改进的线程同步方法。QWaitCondition与QMutex结合,可以使一个线程在满足一定条件时通知其他多个线程,使它们及时作出响应,这样比只使用互斥量效率要高一些。例如,threadDAQ在写满一个缓冲区之后,及时通知threadShow和threadSaveFile,使它们可以及时读取缓冲区数据。

QWaitCondition提供如下一些函数:

  • wait(QMutex*lockedMutex),解锁互斥量lockedMutex,并阻塞等待唤醒条件,被唤醒后锁定lockedMutex并退出函数。
  • wakeAll(),唤醒所有处于等待状态的线程,线程唤醒的顺序不确定,由操作系统的调度策略决定。
  • wakeOne(),唤醒一个处于等待状态的线程,唤醒哪个线程不确定,由操作系统的调度策略决定。

线程类QThreadProducer专门负责掷筛子产生点数,线程类QThreadConsumer专门及时读取数据,并送给主线程进行显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
QMutex mutex;
QWaitCondition newdataAvailable;
int seq=0; //序号
int diceValue;

void QthreadProducer::run(){

m_stop=false;
seq=0;
qsrand(QTime::currentTime().msec()); //随机数初始化

while(!m_stop){ //循环主体
mutex.lock();
diceValue=qrand(); //获取随机数
diceValue=(diceValue%6)+1;
seq++;
mutex.unlock();
newdataAvailable.wakeAll(); //唤醒所有线程,有新数据
msleep(500); //线程休眠

}
}

void QthreadConsumer::run(){

m_stop=false;
while(!m_stop) { //循环主体

mutex.lock();
newdataAvailable.wait(&mutex); //先解锁mutex,使其他线程可以使用mutex
emit newValue(seq,diceValue);
mutex.unlock();

}
}

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
2
3
4
5
QSemaphore wc(5);	//WC.available()==5,初始化资源为5
wc.acquire(4); //wc.available()==1,用了四个资源,剩1个可用
wc.release(2); //wc.available()==3,释放2个资源,剩3个资源可用
wc.tryAcquire(1); //因为wc.available()==0,返回false
wc.acquire(); //因为wc.available()==0,没有资源可用,阻塞

双缓冲区数据采集和读取线程类设计

信号量通常用来保护一定数量的相同的资源,如数据采集时的双缓冲区,适用于Producer/Consumer模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const int BufferSize=8;
int buffer1[BufferSize];
int buffer2[BufferSize];

int curBuf=1; //当前正在写入的Buffer
int bufNo=1; //采集的缓冲区序号
quint8 counter=0; //数据生成器

QSemaphore emptyBufs(2); //信号量,空的缓冲区个数,初始资源个数为2
QSemaphore fullBufs; //满的缓冲区个数,初始资源为0

void QThreadDAQ::run(){

m_stop=false; //启动线程时令m_stop=false
bufNo=0; //缓冲区序号
curBuf=1; //当前写入使用的缓冲区
counter=0; //数据生成器
int n=emptyBufs.available();
if(n<2) //保证线程启动时,emptyBufs.available==2
emptyBufs.release(2-n);

while(!m_stop){//循环主体

emptyBufs.acquire();//获取一个空的缓冲区
for(int i=0;i<BufferSize;i++){ //产生一个缓冲区的数据

if(curBuf==1)
buffer1[i]=counter; //向缓冲区写入数据
else
buffer2[i]=counter;
counter++; //模拟数据采集卡产生数据
msleep(50)

}
bufNo++;//缓冲区序号
if(curBuf==1) //切换当前写入缓冲区
curBuf=2;
else
curBuf=1;
fullBufs.release();//有了一个满的缓冲区,available==1
}
}

void QThreadShow::run(){

m_stop=false;
int n=fullBufs.available();
if(n>0)
fullBufs.acquire(n); //将fullbufs可用资源个数初始化为0
while(!m_stop){
fullBufs.acquire();//等待有缓冲区满,当buffBufs.available==0阻塞
int bufferData[BufferSize];
int seq=bufNo;

if(curBuf==1) //当前在写入的缓冲区是1,那么满的缓冲区是2
for(int i=0;i<BufferSize;i++)
bufferData[i]=buffer2[i];//复制数据
else
for(int i=0;i<BufferSize;i++)
bufferData[i]=buffer1[i];

emptyBufs.release();//缓冲一个空的缓冲区
emit newValue(bufferData,BufferSize,seq);//给主线程传递数据

}
}

信号量emptyBufs初始资源个数为2,表示有2个空的缓冲区可用。

信号量fullBufs初始资源个数为0,表示写满数据的缓冲区个数为零。