一 为什么使用多线程
假设我们的线程任务工作是对一个变量进行++,但是在多线程的情况下由于++操作并不是原子操作,也就是在编译过后,它将被展开成为以下三条机器命令:
- 将变量值载入寄存器
- 将寄存器中的值加1
- 将寄存器中的值写回主存
如果线程1和线程2同时将变量载入寄存器,执行加1操作,然后返回,此时两个线程执行的结果就会相互覆盖,实际上只进行了一次加1操作。
为了保证这种非原子类的操作在多线程的环境下正确执行,我们需要保证上面三条机器指令必须串行执行且不允许中途被打算(原子操作)
1.1 排他性
实际上这种情况普遍来说可以理解为一种临界资源,并且这种临界资源一次仅允许被一个线程使用,他可以是一块内存,一个数据结构,一个文件,或者任何其他排他性使用的东西。
也就是说一次只能有一个线程进入临界区。
二 互斥量
2.1 QMutex
- 对于QMutex类我们只需要注意三个函数 lock unlock 这两个必须成对出现,否则会造成线程死锁。
- qt还提供了一个trylock不同于lock函数,如果这个尝试加锁失败会立即返回,而不会像lock一样阻塞。
2.2 QMutexLocker
- 对于QMutexLocker我们只需要在临界区要进入时使用,当退出函数作用域时,就会自动解锁,这种操作极大程度上避免了线程死锁的情况
三 信号量
- 其实相当于互斥量的扩展,信号量是只能锁定一次而信号量可以获取多次,他可以用来保护一定数量的同种资源。
3.1 用法
- 一个典型用法是控制生产者/消费者之间共享的环形缓冲区
生产者/消费者实例中对同步的需求有两处:
- 如果生产者过快地生产数据,将会覆盖消费者还没有读取的数据
- 如果消费者过快地读取数据,将越过生产者并且读取到一些过期的数据。
针对以上问题,有两种解决方法:
- 首先是生产者填满整个缓冲区,然后等待消费者读取整个缓冲区,这是一种比较笨拙,并且低效的方法
- 生产者和消费者线程分别同时操作缓冲区的不同部分,这是一种比较高效的方法。
3.2 代码示例
- 首先我们定义一系列
- int buffer[BufferSize] : 相当于数据区,也就是上面提到的共享的环形缓冲区,生产者向buffer中写入数据,直到它写到终点,然后再从数组头开始覆盖已经存在的数据。消费者读取前者生产的数据,在此每个int字长都被看成一个资源,实际应用中常会在更大的单位上进行操作,从而减少使用信号量带来的开销。
- freeBytes(BufferSize): 表示可以被生产者写入的缓冲区单元为80个
- useBytes(0):表示可被消费者读取缓冲区的部分;初始化为0表示无数据可读。
const int DataSize=100;
const int BufferSize=80;
int buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes(0);
3.2.1 生产者代码实现
- acquire:中文译为<获取,取得>,也就是说取一个可以使用的空闲单元,如果此时里面freeByte的可用单元为0,也就是说消费者还未处理任何数据,那么这里将会被阻塞,等待消费者处理缓冲区中的数据之后释放可供生产者生产的单元空间。
- for循环: 模拟生产者生产数据,为了不让数组越界所以对下标取余进行控制。一旦生产者获取到可用的空闲单元就会填充这个单元。
- release:将消费者可读的资源加1,切记对于消费者来说,这里的+1是值缓冲区中有一个数据被写入
- 对于acquire其实是对资源-1,release是加1。但是要注意的是生产者信号量的-1,和消费者信号量的+1其实是一对互相作用的,对于生产者来说少了一个可以被写入的空闲单元所以是减一,对消费者信号量来说,多了一个可以被读取的消费单元所以是加1。
- 当然这两个函数都可以跟上指定的n,并且对于acquire也提供了一个tryacquire(n)方法,这个方法如果获取资源失败会立刻返回不会阻塞。
void Producer::run()
{
for(int i=0;i<DataSize;i++)
{
freeBytes.acquire();
buffer[i%BufferSize]=(i%BufferSize);
usedBytes.release();
}
}
3.2.2 消费者代码实现
- acquire:使用消费者信号量来获取资源,获取的是是否有可以读取的资源,也就是非空闲单元,这与生产者是不同的。
- 当获取到有非空闲单元的资源后,开始处理生产者生产的数据
- release:处理完成后,调用生产者信号量的release,让其数据加1,也就是多了一个空闲单元可以被生产者使用。
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");
}
3.2.3 解释
- 对于上面的生产者和消费者信号量其实并不应该这样说。
- 对于上面的生产者信号量准确的来说应该是可写资源信号标识
- 对于上面的生产者信号量准确的来说应该是可读资源信号标识
3.2.4 弊端
- 对于上面这种处理方式,如果我们使用多个消费者,就会导致数据重复处理,当然可以通过优化代码的一些形式来实现多个消费者线程的处理,这可能会比较麻烦。