QThread中的互斥、读写锁、信号量、条件变量
在gemfield的《从pthread到QThread》一文中我们了解了线程的基本使用,但是有一大部分的内容当时说要放到这片文章里讨论,那就是线程的同步问题。关于这个问题,gemfield在《从进 程到线程》中有一个比喻,有必要重新放在下面温习下:
*******************************
最后用一个比喻来总结下:
1、一个进程就好比一个房子里有一个人;
2、clone创建线程就相当于在这个房子里添加一个人;
3、fork创建进程就相当于再造一个房子,然后在新房子里添加一个人;
有了上面的比喻后,我们就清楚很多了:
1、线程之间有很多资源可以共享:比如厨房资源、洗手间资源、热水器资源等;
2、而对于进程来说,一个概念就是进程间通信(你要和另外一个房子里的人通信要比一个房子里的两个人之间通信复杂);
3、线程之间因为共享内存,所以通过一个全局的变量就可以交换数据了;
4、但与此同时,对于线程来说,又有新的概念产生了:
a、一个人使用洗手间的时候,得锁上以防止另一个人对洗手间的访问;
b、一个人(或几个人)睡觉的时候,另外一个人可以按照之前约定的方式来叫醒他;
c、热水器的电源要一直开着,直到想洗澡的人数减为0;
上面的概念,在gemfield的后文中术语化的时候,你就不会再觉得很深奥或者枯燥了。
********************************
对于上面的a:一个人使用洗手间的时候,得锁上以防止另一个人对洗手间的访问。我们在QThread里使用的就是QMutext这个互斥了。mutex是mutual exclusion(互相排斥)的简写。在pthread中也有pthread_mutex_*族,但是在QThread中我们能在Qt的框架下通过源代码看到具体实现,所以pthread_mutex_*就靠你自行研究了。
第一部分、QMutex的研究
1、来一小段代码背景:
************************
int number = 6;
void gemfield1()
{
number *= 5;
number /= 4;
}
void gemfield2()
{
number *= 3;
number /= 2;
}
**************************
如果下面的代码是顺序执行的,则会有下面这样的输出逻辑:
**************************
// gemfield1()
number *= 5; // number 为 30
number /= 4; // number 为 7
// gemfield2()
number *= 3; // number 为 21
number /= 2; // number 为 10
**************************
但如果是在2个线程中(线程1、线程2)分别同时调用了gemfield1()、gemfield2()呢?
**************************
// 线程1调用gemfield1()
number *= 5; // number 为30
// 线程2 调用 gemfield2().
// 线程1 被系统调度出去了,而把线程2调度进来运行
number *= 3; // number 为90
number /= 2; // number 为45
// 线程1 结束运行
number /= 4; // number 为11, 而不是上面的10
**************************
2、如何解决这个问题?
很明显我们想要一个线程(比如线程1)在访问变量number的时候,除非该线程(比如线程1)允许,否则其他线程(比如线程2)不能访问number;这就好比一个人访问洗手间,另一个人就无法访问一样(我们把对number的访问区域,或者洗手间这个区域称作临界区域);下面就是QMutex的使用:
***************************
QMutex mutex;
int number = 6;
void gemfield1()
{
mutex.lock();
number *= 5;
number /= 4;
mutex.unlock();
}
void gemfield2()
{
mutex.lock();
number *= 3;
number /= 2;
mutex.unlock();
}
****************************
当mutex这个互斥lock上之后,直到unlock之前,都只有1个线程访问number;注意:mutex变量和number一样是全局变量!
在QMutex的使用中,我们关注以下4个方法和2个属性:
1、QMutex ()//构造1个mutex
2、lock ()//锁
3、tryLock ()//尝试着锁
4、unlock ()//释放锁
另外两个属性是:递归和非递归。如果这个mutex是递归的话,表明它可以被一个线程锁多次,也就是锁和解锁中再嵌套锁和解锁;非递归的话,就表明mutex只能被锁一次。
这四个的用法已经在上面的代码中展示过了,现在来看看QMutex是怎么做到这一点的?
3、QMutex是如何做到保护临界区域的?
设想一下我们的洗手间问题:洗手间提供了什么机制,让一个人在使用的时候,另一个人无法闯入?门锁!现在开始我们的QMutex之旅:
a、首先得构造出一个QMutex对象吧,要了解这一点,我们得先了解下QMutex的类型层次及成员。
class QBasicMutex
{
public:
inline void lock() {
if (!fastTryLock())
lockInternal();
}
inline void unlock() {
Q_ASSERT(d_ptr.load()); //mutex must be locked
if (!d_ptr.testAndSetRelease(dummyLocked(), 0))
unlockInternal();
}
bool tryLock(int timeout = 0) {
return fastTryLock() || lockInternal(timeout);
}
private:
inline bool fastTryLock() {
return d_ptr.testAndSetAcquire(0, dummyLocked());
}
bool lockInternal(int timeout = -1);
void unlockInternal();
QBasicAtomicPointer<QMutexData> d_ptr;
static inline QMutexData *dummyLocked() {
return reinterpret_cast<QMutexData *>(quintptr(1));
}
friend class QMutex;
friend class QMutexData;
};
————————————————–
class QMutex : public QBasicMutex {
public:
enum RecursionMode { NonRecursive, Recursive };
explicit QMutex(RecursionMode mode = NonRecursive);
};
————————————————–
class QMutexData
{
public:
bool recursive;
QMutexData(QMutex::RecursionMode mode = QMutex::NonRecursive)
: recursive(mode == QMutex::Recursive) {}
};
————————————————–
class QMutexPrivate : public QMutexData {
public:
QMutexPrivate();
bool wait(int timeout = -1);
void wakeUp();
// Conrol the lifetime of the privates
QAtomicInt refCount;
int id;
bool ref() {
Q_ASSERT(refCount.load() >= 0);
int c;
do {
c = refCount.load();
if (c == 0)
return false;
} while (!refCount.testAndSetRelaxed(c, c + 1));
Q_ASSERT(refCount.load() >= 0);
return true;
}
void deref() {
Q_ASSERT(refCount.load() >= 0);
if (!refCount.deref())
release();
Q_ASSERT(refCount.load() >= 0);
}
void release();
static QMutexPrivate *allocate();
QAtomicInt waiters; //number of thread waiting
QAtomicInt possiblyUnlocked; //bool saying that a timed wait timed out
enum { BigNumber = 0×100000 }; //Must be bigger than the possible number of waiters (number of threads)
void derefWaiters(int value);
bool wakeup;
pthread_mutex_t mutex;
pthread_cond_t cond;
};
———————————-
QMutex的类层次上面已经展现了,我们来看看怎么构造一个QMutex对象吧:
QMutex::QMutex(RecursionMode mode)
{
d_ptr.store(mode == Recursive ? new QRecursiveMutexPrivate : 0);
}
其中的d_ptr是在QBasicMutex中定义的:
QBasicAtomicPointer<QMutexData> d_ptr;
根据QMutex构造时的参数,将QMutexData中的recursive成员赋值:默认是0,也就是QMutex::NonRecursive。
b、使用lock(),那么lock()又是怎么实现的呢?
从上面的类型层次可以看出,这个接口是QBasicMutex类实现的,如下:
inline void lock() {
if (!fastTryLock())
lockInternal();
}
也就是说,必须是fastTryLock()返回值为0才有实际的动作,那fastTryLock()又是什么呢?
inline bool fastTryLock() {
return d_ptr.testAndSetAcquire(0, dummyLocked());
}
testAndSetAcquire()又是什么呢?
****************************************************************************************
原型:bool testAndSetAcquire(T *expectedValue, T *newValue);
对于x86平台来说,实现在arch\qatomic_i386.h中:
template <typename T>
Q_INLINE_TEMPLATE bool QBasicAtomicPointer<T>::testAndSetAcquire(T *expectedValue, T *newValue)
{
return testAndSetOrdered(expectedValue, newValue);
}
testAndSetOrdered(expectedValue, newValue)又是怎样实现的?根平台和编译器相关,对于gemfield本文来说,就是 Linux上的GCC编译器,那么:
template <typename T>
Q_INLINE_TEMPLATE bool QBasicAtomicPointer<T>::testAndSetOrdered(T *expectedValue, T *newValue)
{
unsigned char ret;
asm volatile(“lock\n”
“cmpxchgl %3,%2\n”
“sete %1\n”
: “=a” (newValue), “=qm” (ret), “+m” (_q_value)
: “r” (newValue), “0″ (expectedValue)
: “memory”);
return ret != 0;
}
**************************************************************************************
d_ptr.testAndSetAcquire(0, dummyLocked());的含义就是判断d_ptr当前的值是不是0,如果是0的话,则将dummyLocked()的值赋给d_ptr,并返回真值;否则什么都不做,并返回false。
static inline QMutexData *dummyLocked() {
return reinterpret_cast<QMutexData *>(quintptr(1));
}
对不起了,各位,我刚洗了个澡回来。我发现照这样写下去本文就写不完了。我决定把本文介绍的内容的底层实现部分放在《Qt的原子操作》一文之后,本文从简介绍下互斥、读写锁、条件变量、信号量这些概念及用法。所以,上面红颜色装饰的内容就先不要看了。
第二部分、QMutexLocker的诞生
QMutexLocker相当于QMutex的简化,提供了简化了的互斥上的操作(也即简化了的加锁和解锁)。
QMutex实现的互斥功能用的不是挺好的吗?怎么又出现了一个QMutexLocker?其实不然,观察下面的这个代码:
****************************************************************
int complexFunction(int flag)
{
mutex.lock();
int retVal = 0;
switch (flag) {
case 0:
case 1:
mutex.unlock();
return moreComplexFunction(flag);
case 2:
{
int status = anotherFunction();
if (status < 0) {
mutex.unlock();
return -2;
}
retVal = status + flag;
}
break;
default:
if (flag > 10) {
mutex.unlock();
return -1;
}
break;
}
mutex.unlock();
return retVal;
}
*******************************************************************
上面的代码真实的揭露了QMutex的无力,因为只要有mutex.lock(),必然要有mutex.unlock(),否则临界区里的资源将再不能被访问;而上面的代码并不能保证QMutex的对象一定会unlock(代码可能从某个地方就走了,再不回来了)。这个时候QMutexLocker就发挥用处了,因为QMutexLocker一定是以函数内部的局部变量的形式出现的,当它的作用域结束的时候,这个互斥就自然unlock了。代码如下:
*******************************************************************
int complexFunction(int flag)
{
QMutexLocker locker(&mutex);//定义的时候就上锁了
int retVal = 0;
switch (flag) {
case 0:
case 1:
return moreComplexFunction(flag);
case 2:
{
int status = anotherFunction();
if (status < 0)
return -2;
retVal = status + flag;
}
break;
default:
if (flag > 10)
return -1;
break;
}
return retVal;//超出函数的作用域就解锁了
}
******************************************************************
第三部分:QReadWriteLock的作用
虽然互斥的功能保证了临界区资源的安全,但是在某些方面并不符合实际;比如一般情况下,资源可以被并发读!举个实际的例子:有一本书(比如CivilNet BOOK),当某个人读到一页时,另外一个人(或者多个人)也可以过来读;但是,当1个人往上面写笔记时,其他人不能一起写,而且只有这个人把笔记写完了,再让大家一起看。
QReadWriteLock的作用就是保证各个线程能并发的读某个资源,但是要写的话,就得真的lock了(所以,QReadWriteLock适合大量并发读,偶尔会有写的情况);代码如下:
*************************************************************
QReadWriteLock lock;
void ReaderThread::run()
{
…
lock.lockForRead();
read_file();
lock.unlock();
…
}
void WriterThread::run()
{
…
lock.lockForWrite();
write_file();
lock.unlock();
…
}
**************************************************************
特别的,对于lock这个全局锁来说:
1、只要有任意一个线程lock.lockForWrite()之后,所有之后的lock.lockForRead()都将会被阻塞;
2、只要有任意一个线程的lock.lockForWrite()动作还在被阻塞着的话,所有之后的lock.lockForRead()都会失败;
3、如果在被阻塞的队列中既有lock.lockForWrite()又有lock.lockForRead(),那么write的优先级比read高,下一个执行的将会是lock.lockForWrite()。
大多数情况下,QReadWriteLock都是QMutex的直接竞争者.和QMutex类似,QReadWriteLock也提供了它的简化类来应付复杂的加锁解锁(也是通过函数作用域的手段),代码如下:
****************************************************************
QReadWriteLock lock;
QByteArray readData()
{
QReadLocker locker(&lock);
…
return data;
}
void writeData(const QByteArray &data)
{
QWriteLocker locker(&lock);
…
}
*************************************************************
第四部分:QSemaphore 提供了QMutex的通用情况
反过来,QMutex是QSemaphore的特殊情况,QMutex只能被lock一次,而QSemaphore却可以获得多次;当然了,那是因为Semaphores要保护的资源和mutex保护的不是一类;Semaphores保护的一 般是一堆相同的资源; 比如:
1、mutex保护的像是洗手间这样的,只能供1人使用的资源(不是公共场所的洗手间);
2、Semaphores保护的是像停车场、餐馆这样有很多位子资源的场所;
Semaphores 使用两个基本操作acquire() 和 release():
比如对于一个停车场来说,一般会在停车场的入口用个LED指示牌来指示已使用车位、可用车位等;你要泊车进去,那就要acquire(1)了,这样available()就会减一;如果你开车离开停车场 ,那么就要release(1)了,同时available()就会加一。
让gemfield用代码来演示一个环形缓冲区和其上的信号量(生产-消费模型):
const int DataSize = 1000;//这个店一共要向这个环形桌上供应1000份涮肉
const int BufferSize = 100;//环形自助餐桌上可以最多容纳下100份涮肉
char buffer[BufferSize];//buffer就是这个环形自助餐桌了
QSemaphore freePlace(BufferSize);//freeBytes信号量控制的是没有放涮肉盘子的区域,初始化值是100,很明显,程序刚开始的时候桌子还是空的;
QSemaphore usedPlace;//usedPlace 控制的是已经被使用的位置,也很明显,程序刚开始的时候,还没开始吃呢。
好了,对于饭店配羊肉的服务员来说,
class Producer : public QThread
{
public:
void run();//重新实施run虚函数
};
void Producer::run()
{
for (int i = 0; i < DataSize; ++i) {
freePlace.acquire();//空白位置减一
buffer[i % BufferSize] = “M”;//放肉(麦当劳 )
usedPlace.release();//已使用的位置加一
}
}
服务员(producer)要生产1000份涮肉( DataSize), 当他要把生产好的一份涮肉往环形桌子上放之前,必须使用freePlace信号量从环形桌上获得一个空地方(一共就100个)。 如果消费 者吃的节奏没跟的上的话,QSemaphore::acquire() 调用可能会被阻塞。
最后,服务员使用usedPlace信号量来释放一个名额。一个“空的位置”被成功的转变为“已被占用的位置”,而这个位置消费者正准备吃。
对于食客(消费者)来说:
class Consumer : public QThread
{
public:
void run();//重新实施run虚函数
};
void Consumer::run()
{
//消费者一共要吃1000份涮肉
for (int i = 0; i < DataSize; ++i) {
usedPlace.acquire();//如果还没有位置被使用(表明没有放肉),阻塞
eat(buffer[i % BufferSize]);
freePlace.release();//吃完后,空白位置可以加一了
}
leaveCanting();
}
在main函数中,gemfield创建了2个线程,并且通过QThread::wait()来确保在程序退出之前,线程都已经执行完了(也即完成了各自的1000次for循环)
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}
程序是怎么运行的呢?
初始的时候,只有服务员线程可以做任何事; 消费者线程被阻塞了——等待着usedPlace信号量被释放(available()初始值是0);当服务员把第一份涮肉放到桌子上的时候,
freePlace.available() 的值就变为了BufferSize – 1, 并且usedPlace.available() is 1.这时,两个线程都可以工作了: 消费者可以吃这第一份涮肉,并且服务员再生产第二份涮肉;
在一个多处理器的机器上,这个程序将有可能达到基于mutex的程序的2倍快, 因为两个线程可以同时工作在不同的缓冲区上。
第五部分: QWaitCondition,与QSemaphore的竞争
const int DataSize = 1000;//这个店一共要向这个环形桌上供应1000份涮肉
const int BufferSize = 100;//环形自助餐桌上可以最多容纳下100份涮肉
char buffer[BufferSize];
QWaitCondition placeNotEmpty;//当有肉放上来,就发出这个信号
QWaitCondition placeNotFull;//当消费者吃完一份涮肉后发出这个信号
QMutex mutex;
int numUsedPlace = 0;//已经放了肉的位置数
为了同步服务员和消费者, 我们需要2个条件变量和1个mutex。变量解释参考上面的代码注释。让我们看看服务员这个生产者的类:
************************************************
class Producer : public QThread
{
public:
void run();
};
void Producer::run()
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == BufferSize)
placeNotFull.wait(&mutex);
mutex.unlock();
buffer[i % BufferSize] = “M”;(又是麦当劳)
mutex.lock();
++numUsedBytes;
placeNotEmpty.wakeAll();
mutex.unlock();
}
}
******************************************************
在服务员将肉放到环形桌上之前,先要检查下桌子是不是放满了。如果满了,服务员就等待placeNotFull条件.
在最后,服务员将numUsedBytes自增1,并且发出bufferNotEmpty条件是真的这个信号,因为numUsedBytes肯定大于0;
注意,QWaitCondition::wait() 函数使用一个mutex作为它的参数,这样做的意义是:mutex刚开始是lock的,然后当这个线程因为placeNotFull.wait(&mutex);而休眠时,这个mutex就会被unlock,而当这个线程被唤醒时,mutex再次被加锁。
另外, 从locked状态到wait状态是原子的,以此来防止竞态条件的发生。
再来看看消费者类:
**************************************************
class Consumer : public QThread
{
public:
void run();
};
void Consumer::run()
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == 0)
placeNotEmpty.wait(&mutex);
mutex.unlock();
eat(buffer[i % BufferSize]);
mutex.lock();
–numUsedBytes;
placeNotFull.wakeAll();
mutex.unlock();
}
leaveCanting();
}
***************************************************
代码和服务员的差不多。再来看看main函数:
***************************************************
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}
***************************************************
和信号一节差不多,程序刚开始的时候,只有服务员线程可以做一些事;消费者被阻塞(等肉),直到“位置不为空”信号发出。
在一个多处理器的机器上,这个程序将有可能达到基于mutex的程序的2倍快, 因为两个线程可以同时工作在不同的缓冲区上。
其实,Qt的线程库所包含的内容正是gemfield上一文《从pthread到QThread》中介绍的QThread类,以及QMutexLocker, QReadWriteLock, QSemaphore, QWaitCondition这些类,再外加一个atomic原子操作的内容(这时gemfield下一篇文章的内容哦);了解了这些,我们就可以更加自信的使用Qt的线程库了。
备注:本文属于gemfield的CivilNet博客(http://syszux.com/blog)[Qt乐园]版块,转载此文时,请保证包括备注在内的本文的完整性。