一、线程同步概念
二、QT线程同步技术简介
2.1初级锁
2.2读写锁
2.3条件锁
三、QT线程同步应用详解
3.1初级锁
3.2读写锁
3.3条件锁
线程同步,广义上的概念一般指多线程间对资源“读”与“写”权限的管理策略,为了避免数据冲突与数据不一致,关于此概念,网上介绍很多,在此不赘述。
对需要保护的资源上锁,独占式使用,如:公厕的蹲坑,一次只能一人用,你进入了,你就会反锁,别人就进不去,这就是初级的独占式上锁。
QT提供了相关的类QMutex,有lock和unlock函数,成对使用。
为了更高效的利用资源,QT提供了读写权限分离锁QReadWriteLock,有以下4种应用场景:
A:我在读的时候,你也可以读:一起看看无所谓,又不会看坏;
B:我在读的时候,你不可以写:我读的时候,你不能动手,以免我看得眼花缭乱;
C:我在写的时候,你不可以读:我动手时,你别看,以免你看不真切;
D:我在写的时候,你不可以写:我动手时,你可别动手,搞不清楚到底谁动的手;
以上,写的时候必须独占,读的时候可以共享读权限,但是也不能写,此机制提高了读的效率,简而言之,大家一起看看是可以的,但是谁都不能动手(修改)
我也不知道应该怎么命名,所谓“条件锁”是我自己起的名,即用于线程之间同步,A线程下一步动作,依赖于B线程的任务完成后,及时通知A线程的一种机制。
QT提供了此机制QWaitCondition,此机制结合QMutex与QWaitCondition两个类,让多个线程实现时序运行。
QMutex是QT封闭的一个互斥量,上锁操作提供两个函数lock与tryLock,前者阻塞方式死等,直到获取到锁的,后者可以设定等待时间,时间到了将返回;解锁操作函数为unlock函数。
其实,lock与unlock是成对使用的,使用不当将造成严重后果。像new与delete一样,如果new了忘记了delete将造成内存泄漏,如果lock了,忘记了unlock将造成“死锁”,这问题是严重的,而且很难定位,那我们应该怎么规避呢?
QT提供了一个类QMutexLocker解决此问题,它的策略很简单,用此类实例化一个对象的时候,需要把一个QMutex对象的指针传给它,在构造函数中,它就调用lock给加锁了,然后此对象析构时,在析构函数中会调用unlock,自动释放,如此,将永远都不可能出现死锁的情况了。
QMutex mutex;
void func()
{
//实例化locker时,构造函数会对mutex加锁
QMutexLocker locker(&mutex);
//do something
//因为locker是个局部变量,此函数退出时,
//会析构,自然就会对mutex解锁
}
这些设计是不是很巧妙?又非常简单。
好的编程习惯,应该禁止使用直接调用lock、unlock,应该采用以上方法。
读写锁的应用场景,2.2已经介绍,下面看代码实例:
QReadWriteLock lock;
void ReaderThread::run()
{
lock.lockForRead();
read_file();
lock.unlock();
}
void WriterThread::run()
{
lock.lockForWrite();
write_file();
lock.unlock();
}
如3.1中介绍一样,里面获取锁与释放锁调用调用lock与unlock,将有死锁的风险,这不是严谨的编码风险,同时QT对读写锁与提供了如QMutexLocker一样功能的类,对应读、写锁分别是
QReadLocker、QWriteLocker,所以,以上代码应该改成如下:
QReadWriteLock lock;
void ReaderThread::run()
{
QReadLocker locker(&lock);
read_file();
}
void WriterThread::run()
{
QWriteLocker locker(&lock);
write_file();
}
切记,不要写出高风险代码,不想随意相信手册与书本教的东西,实践中的得出的方法,才
是王道、
我们设计这么一种场景,SendThead线程负责往TCP服务端发送数据,RecvThread线程负责从TCP服务端接收数据。
SendThread发送数据后,需要得到TCP服务端的回应后,再执行下一步操作,所以,此场景下SendThread与RecvThread之间有一个时序关系,即SendThread线程的下一步动作,需要依赖RecvThread线程的结果,代码如下:
青铜写法:
QReadWriteLock lock;
void SendThread()
{
lock.lock();
SendMsg();
QWaitCondition waitCdn;
waitCdn.wait(&lock);
//do something
lock.unlock();
}
void RecvThread()
{
RecvMsg();
lock.lock();
QWaitCondition waitCdn;
waitCdn.wakeAll(&lock);
lock.unlock();
}
王者写法:
QReadWriteLock lock;
void SendThread()
{
QMutexLocker locker(&lock);
SendMsg();
QWaitCondition waitCdn;
waitCdn.wait(&lock);
//have recv the Ack, do something
}
void RecvThread()
{
RecvMsg();
QMutexLocker locker(&lock);
QWaitCondition waitCdn;
waitCdn.wakeAll(&lock);
}
QWaitCondition的wait函数,是会对传入的mutex互斥量,进行解锁,然后wait函数自身会阻塞,进到QWaitCondition的wakeAll或者wakeOne被调用,wait函数又马上对互斥量进行加锁。
下面我们对以上代码进行解析,首先SendThread线程加锁,然后调用发送数据后,启用wait函数,等待RecvThread接收数据,此时wait对互斥量进行解锁,然后自己阻塞。
下面,我们再看RecvThread,它先进行接收数据,然后,再加锁(此时,此锁被SendThread的wait函数给解了),然后再调用wakeAll函数。触发wait函数加锁,然后返回,SendThread此时又将继续往下执行。
RecvThread为什么要加锁?因为,此处不加锁,可能导致wakeAll比wait函数先调用的极端情况,导致wait永远无法返回,从而,造成死锁。所以,当使用QWaitCondition编程时,如果没有此机制,此代码是风险代码,是不过关的,可以说,水平是非常次的。