实现线程的互斥与同步常使用的类有QMutex、QMutexLocker、QReadWriteLocker、QReadLocker、
QWriteLocker、QSemaphore和QWaitCondition。
下面举一个例子加以说明:
class Key
{
public:
Key() {key=0;}
int creatKey() {++key; return key;}
int value()const {return key;}
private:
int key;
};
在多线程环境下,这个类是不安全的,因为存在多个线程同时修改私有成员key,其结果是不可预知
的。
虽然Key类产生主键的函数creatKey()只有一条语句执行修改成员变量key的值,但是C++的“++”操作符
并不是原子操作,通常编译后,它将被展开成为以下三条机器命令:
(1)将变量值载入寄存器。
(2)将寄存器中的值加1。
(3)将寄存器中的值写回主存。
一、互斥量
1、QMutex类
QMutex类的lock()函数用于锁住互斥量。如果互斥量处于解锁状态,则当前线程就会立即抓住并锁定它,否则当前线程就会被阻塞,直到持有这个互斥量的线程对它解锁。线程调用lock()函数后就会持有这个互斥量,直到调用unlock()操作为止。 QMutex类还提供了一个tryLock()函数。如果互斥量已被锁定,则立即返回。 例如:
class Key
{
public:
Key() {key=0;}
int creatKey() { mutex.lock(); ++key; return key; mutex. unlock();}
int value()const { mutex.lock(); return key; mutex.unlock();}
private:
int key;
QMutex mutex;
};
2、QMutexLocker类
Qt提供的QMutexLocker类可以简化互斥量的处理,它在构造函数中接收一个QMutex对象作为参数并将其锁定,在析构函数中解锁这个互斥量,这样就解决了以上问题。
例如:
class Key
{
public:
Key() {key=0;}
int creatKey() { QmutexLocker locker(&mutex); ++key; return key; }
int value()const { QmutexLocker locker(&mutex); return key; }
private:
int key;
QMutex mutex;
};
locker()函数作为局部变量会在函数退出时结束其作用域,从而自动对互斥量mutex解锁。
二、信号量
信号量可以理解为对互斥量功能的扩展,互斥量只能锁定一次而信号量可以获取多次,它可以用来保护一定数量的同种资源。信号量的典型用例是控制生产者/消费者之间共享的环形缓冲区。 生产者/消费者实例中对同步的需求有两处:
(1)如果生产者过快地生产数据,将会覆盖消费者还没有读取的数据。
(2)如果消费者过快地读取数据,将越过生产者并且读取到一些过期数据。
针对以上问题,有两种解决方法:
(1)首先使生产者填满整个缓冲区,然后等待消费者读取整个缓冲区,这是一种比较笨拙的方 法。
(2)使生产者和消费者线程同时分别操作缓冲区的不同部分,这是一种比较高效的方法。
具体使用方法见如下代码:
(1)在源文件“main.cpp”中添加的具体实现代码如下:
#include <QCoreApplication>
#include <QSemaphore>
#include <QThread>
#include <stdio.h>
const int DataSize=1000;
const int BufferSize=80;
int buffer[BufferSize]; //首先,生产者向buffer中写入数据,直到它到达终点,然后从起点重新开始覆盖已经存在的数据。消费者读取前者生产的数据,在此处每个int字长都被看成一个资源,实际应用中常会在更大的单位上进行操作,从而减少使用信号量带来的开销。
QSemaphore freeBytes(BufferSize); // freeBytes信号量控制可被生产者填充的缓冲区部分,被初始化为BufferSize(80),表示程序一开始有BufferSize个缓冲区单元可被填充。
QSemaphore usedBytes(0); // usedBytes信号量控制可被消费者读取的缓冲区部分,被初始化为0, 表示程序一开始时缓冲区中没有数据可供读取。
(2)Producer类继承自QThread类,作为生产者类,其声明如下:
class Producer : public QThread
{
public:
Producer();
void run();
};
Producer构造函数中没有实现任何内容:
Producer::Producer()
{
}
Producer::run()函数的具体实现代码如下:
void Producer::run()
{
for(int i=0;i<DataSize;i++)
{
freeBytes.acquire(); //生产者线程首先获取一个空闲单元,如果此时缓冲区被消费者尚未读取的数据填满,对此函数的调用就会阻塞,直到消费者读取了这些数据为止。
buffer[i%BufferSize]=(i%BufferSize); //一旦生产者获取了某个空闲单元,就使用当前的缓冲区单元序号填写这个缓冲区单元。
usedBytes.release(); //调用该函数将可用资源加1,表示消费者此时可以读取这个刚刚填写的单元了。
}
}
release(n)函数用于释放n个资源。
(3)Consumer类继承自QThread类,作为消费者类,其声明如下:
class Consumer : public QThread
{
public:
Consumer();
void run();
};
Consumer构造函数中没有实现任何内容:
Consumer::Consumer()
{
}
Consumer::run()函数的具体实现代码如下:
void Consumer::run()
{
for(int i=0;i<DataSize;i++)
{
usedBytes.acquire(); //消费者线程首先获取一个可被读取的单元,如果缓冲区中没有包含任何可以读取的数据,对此函数的调用就会阻塞,直到生产者生产了一些数据为止。
fprintf(stderr,"%d",buffer[i%BufferSize]); //一旦消费者获取了这个单元,会将这个单元的内容打印出来。
if(i%16==0&&i!=0)
fprintf(stderr,"\n");
freeBytes.release(); //调用该函数使得这个单元变为空闲,以备生产者下次填充。
}
fprintf(stderr,"\n");
}
(4)main()函数的具体内容如下:
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Producer producer;
Consumer consumer;
/* 启动生产者和消费者线程 */
producer.start();
consumer.start();
/* 等待生产者和消费者各自执行完毕后自动退出 */
producer.wait();
consumer.wait();
return a.exec();
}
(5)最终运行结果如下图所示: