Qt中的线程同步
qt中实现了如下类,提供线程同步机制
- QMutex:互斥量。用来确保同一时刻,只能有一个线程访问某一资源。
- QReadWriteLock:读写锁。允许同一时刻多个线程读取某一资源,但只要有一个线程在写该资源,则不允许其他程同时读取该资源。
- QWaitCondition:等待条件。提供了一个条件变量同步线程,当某个条件满足时,可以发送信号通知其他线程该条件已经达到。
- QSemphone:信号量。能够保证同一时刻一个或多个资源不被并发访问,经常用在有限的相同资源条件下的线程同步。
线程同步之QMutex
Qt为我们提供了一系列的容器类,比如QList,QQueue,QStack等,但都是可重入(reentrant)而不是线程安全的(thread-safe)。因此当遇到多线程的容器共享时,我们就需要实现自己的线程安全版本容器,以QQueue为例:
QSafeQueue.h
#include <QList>
#include <QMutex>
template<typename T>
class QSafeQueue
{
public:
QSafeQueue();
void push_back(const T& data);
T pop_front();
private:
QList<T> m_oList;
QMutex m_oMutex;
};
QSafeQueue.cpp
template<typename T>
QSafeQueue<T>::QSafeQueue()
: m_oList(),
m_oMutex()
{
}
template<typename T>
void QSafeQueue<T>::push_back(const T &data)
{
m_oMutex.lock();
m_oList.push_back(data);
m_oMutex.unlock();
}
template<typename T>
T QSafeQueue<T>::pop_front()
{
if (m_oList.empty())
throw;
m_oMutex.lock();
T temp = m_oList.front();
m_oList.pop_front();
m_oMutex.unlock();
return temp;
}
针对上面的实现,考虑这样一种情况,假设队列很长,并且在某些时候从队列中读取数据的线程要远远多于向队列中添加数据,此时读线程的锁就会影响写线程,从而降低队列的执行效率,为此我们需要改进安全队列,使用读写两个不同的锁,看下列实现:
QSafeQueue.h
#include <QList>
#include <QMutex>
template<typename T>
class QSafeQueue
{
public:
QSafeQueue();
void push_back(const T& data);
T pop_front();
private:
QList<T> m_oList;
QMutex m_oReadMutex;
QMutex m_oWriteMutex;
};
QSafeQueue.cpp
template<typename T>
QSafeQueue<T>::QSafeQueue()
: m_oList(),
m_oMutex()
{
}
template<typename T>
void QSafeQueue<T>::push_back(const T &data)
{
m_oWriteMutex.lock();
m_oList.push_back(data);
m_oWriteMutex.unlock();
}
template<typename T>
T QSafeQueue<T>::pop_front()
{
if (m_oList.empty())
throw;
m_oReadMutex.lock();
T temp = m_oList.front();
m_oList.pop_front();
m_oReadMutex.unlock();
return temp;
}
QMutex有两种模式:
- Recursive:这种模式下,要求lock和unlock必须同步出现,即调用了多少次lock就必须调用多少次unlock。
- NonRecursive:这种模式下,只允许线程只能锁一次,默认是这种模式。
Qt提供了QMutexLocker来简单的实现QMutex的锁定与解锁,可以将上面push_back方法改为如下代码,pop方法同样适用
template<typename T>
void QSafeQueue<T>::push_back(const T &data)
{
QMutexLocker mutexLocker(&m_oMutex);
m_oList.push_back(data);
}
线程同步之QWaitCondition
在上面的实现中,当队列为空时,我们直接抛出了一个异常,但有时我们希望当队列为空时线程等待,直到有数据为止(这种队列又称为阻塞队列)。这时我们就应该使用条件变量。QWaitCondition必须与QMutex配合使用不能单独使用。看如下实现:
QBlockingQueue.h
#include <QList>
#include <QMutex>
#include <QWaitCondition>
template <typename T>
class QBlockingQueue
{
public:
QBlockingQueue();
void push_back(const T& data);
T pop_front();
private:
QList<T> m_oList;
QMutex m_oMutex;
QWaitCondition m_oWaitCondition;
};
QBlockingQueue.cpp
template<typename T>
QBlockingQueue<T>::QBlockingQueue()
: m_oList(),
m_oMutex(),
m_oWaitCondition()
{
}
// 首先判断队列是否为空,如果队列为空时,当加入新的数据时,调用条件等待的wakeAll唤醒
// 其他阻塞的读线程,如果有多个读线程,只能唤醒一个线程,唤醒线程的顺序一般先唤醒高优先级的,
// 如果优先级相同,则唤醒等待时间长的那个
template<typename T>
void QBlockingQueue<T>::push_back(const T &data)
{
m_oMutex.lock();
bool bEmpty = m_oList.empty();
m_oList.push_back(data);
m_oMutex.unlock();
if (bEmpty)
m_oWaitCondition.wakeAll();
}
// 当队列为空时等待,QWaitCondition还会释放QMutex(内部会调用unlock)
// 其他调用此方法的线程也阻塞在等待条件这里
template<typename T>
T QBlockingQueue<T>::pop_front()
{
m_oMutex.lock();
// 在这里应该使用while而不应该使用if
// 因为操作系统有时会出现虚假唤醒
while (m_oList.empty())
{
m_oWaitCondition(&m_oMutex);
}
T temp = m_oList.front();
m_oList.pop_front();
m_oMutex.unlock();
return temp;
}
在某些情况下(某些实时系统),当我们不能在一段时间内处理完数据,将丢弃这些数据,只处理最新的数据这时就需要对我们的阻塞队列增加超时限制(重载的push_back和pop_front):
bool QBlockingQueue<T>::push_back(const T &data, const int milisec)
{
int nTick1 = GetTickCount();
QMutexLocker mutexLocker(&m_oMutex);
int nTick2 = GetTickCount();
if (nTick2 - nTick1 < milisec)
{
bool bEmpty = m_oList.empty();
m_oList.push_back(data);
if (bEmpty)
m_oWaitCondition.wakeAll();
return true;
}
return false;
}
template<typename T>
T QBlockingQueue<T>::pop(const int milisec)
{
int nTick1 = GetTickCount();
QMutexLocker mutexLocker(&m_oMutex);
int nTick2 = GetTickCount();
if (nTick2 - nTick1 < milisec)
{
bool bResult = true;
while (m_oList.empty() && bResult)
{
// QWaitCondition如果是被唤醒,则返回true
// 如果是超时(参数2),则返回false
bResult = m_oWaitCondition.wait(&m_oMutex, milisec - (nTick2 - nTick1));
}
if (bResult)
{
T temp = m_oList.front();
m_oList.pop_front();
return temp;
}
}
throw "time out";
}
还有一些情况,队列不能无限大(例如仓库的大小都是有限制的),这时我们就需要有大小的阻塞队列,看如下新的实现:
QBlockingQueue.h
#include <QList>
#include <QMutex>
#include <QWaitCondition>
template <typename T>
class QBlockingQueue
{
public:
QBlockingQueue(int nMaxSize);
void push_back(const T& data);
T pop_front();
private:
int m_nMaxSize;
QList<T> m_oList;
QMutex m_oMutex;
QWaitCondition m_oReadWaitCondition;
QWaitCondition m_oWriteWaitCondition;
};
QBlockingQueue.cpp
template<typename T>
QBlockingQueue<T>::QBlockingQueue(int nMaxSize)
: m_oList(),
m_oMutex(),
m_oReadWaitCondition(),
m_oWriteWaitCondition(),
m_nMaxSize(nMaxSize)
{
}
template<typename T>
void QBlockingQueue<T>::push_back(const T &data)
{
m_oMutex.lock();
bool bEmpty = m_oList.empty();
while (m_oList.count() == m_nMaxSize)
{
m_oWriteWaitCondition.wait(&m_oMutex);
}
m_oList.push_back(data);
m_oMutex.unlock();
if (bEmpty)
m_oReadWaitCondition.wakeAll();
}
template<typename T>
T QBlockingQueue<T>::pop_front()
{
m_oMutex.lock();
bool bFull = m_oList.count() == m_nMaxSize ? true : false;
while (m_oList.empty())
{
m_oReadWaitCondition.wait(&m_oMutex);
}
T temp = m_oList.front();
m_oList.pop_front();
m_oMutex.unlock();
if (bFull)
m_oWriteWaitCondition.wakeAll();
return temp;
}
线程同步之QReadWriteLock
针对大多数的容器比如QList,我们常用的操作都是读取数据,一般插入和删除操作都比较少,这时我们使用QMutex就会大大降低效率,此时我们就需要用读写锁,看如下代码:
QSafeList.h
#include <QList>
#include <QReadWriteLock>
template <typename T>
class QSafeList
{
public:
QSafeList();
T at(int i);
void append(const T& data);
private:
QList<T> m_oList;
QReadWriteLock m_oRWLock;
};
QSafeList.cpp
template <typename T>
QSafeList<T>::QSafeList()
: m_oList(),
m_oRWLock()
{
}
template <typename T>
T QSafeList<T>::at(int i)
{
m_oRWLock.lockForRead();
if (i < 0 || i >= m_oList.count())
{
m_oRWLock.unlock();
throw "out of bounds";
}
T temp = m_oList[i];
m_oRWLock.unlock();
return temp;
}
template <typename T>
void QSafeList<T>::append(const T &data)
{
m_oRWLock.lockForWrite();
m_oList.append(data);
m_oRWLock.unlock();
}
这里仅实现了at和append方法,有兴趣的可以实现下QList的其他一些方法,这里注意QList的at方法返回的是const T&,而我们这边返回的是T,在线程安全的类中一般不返回引用,因为在使用中有可能被其他线程释放掉了。QT对读写锁也提供了渐变使用的QReadLocker和QWriteLocker类,因此上面的方法可以写为:
template <typename T>
T QSafeList<T>::at(int i)
{
QReadLocker readLocker(&m_oRWLock);
if (i < 0 || i >= m_oList.count())
{
throw "out of bounds";
}
return m_oList[i];
}
template <typename T>
void QSafeList<T>::append(const T &data)
{
QWriteLocker writeLocker(&m_oRWLock);
m_oList.append(data);
}
线程同步之QSemphone
信号量有时又称为信号灯,更像一种更一般化的Mutex,当信号量管理的资源只有一个时,与互斥量基本没有区别,但是互斥量的lock和unlock必须在一个线程内调用,而信号量的aquire与release可以在不同的线程调用,因此信号量还能够起到同步线程的作用。