Linux多线程间同步与互斥---条件变量(Conditoin Variable)

时间:2021-04-18 18:09:53

同步和互斥:

前言:线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点也是重点。

 linux下提供了多种方式来处理线程同步,最常用的是互斥量(mutex)、条件变量(Conditoin Variable)和信号量(Semaphore)。

如果能看到这里,相信都对这三种方式有所了解,在此,我只阐述条件变量这一块最难理解的地方,如果有误区,还请大神不吝指教:

条件变量:

条件变量的作用是用于多线程之间关于共享数据状态变化的通信。

当一个动作需要另外一个动作完成时才能进行,即:

当一个线程的行为依赖于另外一个线程对共享数据状态的改变时,这时候就可以使用条件变量

不理解?我们假设第一种情况:假设没有条件变量:

  对于一个生产者消费者问题,消费线程在得知队列中没有产品时,将阻塞自己(就是挂起等待)。生产线程给队列中放入产品,但是没有办法激活消费线程,而消费线程处于阻塞(挂起)状态也没有办法自激活。如果消费线程使用忙等(24小时不间断询问)的方式,通过不断地查询来判断是否有产品将大量的浪费CPU时间。消费线程可以使用睡眠+查询(每隔一段时间)的方式,即发现队列中没有产品时,sleep一段时间,然后再查询。问题是睡眠多长时间?时间太长,实时性不好,时间太短,还是浪费CPU时间。

所以,通过生产线程通过唤醒消费线程时最好的方式。

Linux多线程间同步与互斥---条件变量(Conditoin Variable)

现在我们考虑一种实现,消费线程在阻塞之前要先解锁(个人想法:这句话的意思是,消费线程已经获得了要访问资源的锁,但是,即使我获得了资源的锁,但是由于条件暂时还不满足,我无法用这个资源,所以我想暂时让出这把锁,让之里的资源暂时为别人所用,所以在挂起前,我需要解锁),同时还要将自己的标识符放入一个地方,以便生产线程通过这个标识符来激活自己。那么问题就来了,由于线程之间是并发/并行的。消费线程可能刚完成解锁的操作,就被生产线程获取到了并开始执行,这时,因为消费线程还未挂起自己,来不及将自己的标识符保存在某个位置,所以生产线程不认为有正在等待的线程(生产线程想告诉消费线程的唯一方式就是认消费线程的标识符)。这时,切换到消费线程后,消费线程将永远的等待下去,虽然队列中有产品,但生产线程也不会告诉消费线程。而生产线程因为队列中有产品可能也一直的等待下去,形成了死锁

这里死锁的原因很明确,就是因为消费线程在阻塞之前要先解锁解、保存线程标识符、挂起这一系列操作不是原子操作。想要让这一些列的操作成为原子操作,就得引入条件变量,所以不难想到使用条件变量的时候必须要“伴随”一个互斥量。

条件变量是与互斥量相关联的一种用于多线程之间关于共享数据状态改变的通信机制。它将解锁和挂起封装成为原子操作。等待一个条件变量时,会解开与该条件变量相关的锁,因此,使用条件变量等待的前提之一就是保证互斥量加锁。线程醒来之后,该互斥量会被自动加锁,所以,在完成相关操作之后需要解锁。
用条件变量配合互斥量实现,条件变量与互斥量结合,使得在条件不满足的情况下,能够释放对缓冲区的占用,使得他人能够访问缓冲区。当我添加满足时,我又可以及时的加锁之后独占资源的完成我自己的工作。
关于实践,直接用源码结束,下面是一点拙见:
条件变量与互斥量配合以实现线程的同步:
 #include "stdio.h"  
#include <stdlib.h>
#include <pthread.h>

#define Num_CONSUMER 2 //消费者数量
#define Num_PRODUCER 2 //生产者数量
#define C_SLEEP 1 //Consumer
#define P_SLEEP 1 //producer

pthread_t ctid[Num_CONSUMER];//消费者线程 id
pthread_t ptid[Num_PRODUCER];//生产者线程 id

pthread_cond_t notFull,notEmpty;//缓冲有无东西
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //用于锁住缓冲区

//从 begin 到 end(不含end) 代表产品
//cnt 代表产品数量
//max 代表库房的容量,即最多生产多少产品
int begin = 0,end = 0, cnt = 0, max = 4;

void * Consumer(void * pidx)
{
printf("Consumer thread id %d\n",*((int *)pidx));
while(1)
{
pthread_mutex_lock(&mutex);
while(cnt == 0)
{//当缓冲区空时
pthread_cond_wait(&Empty,&mutex); //等待条件成立
}
printf("consume %d\n",begin);
begin = (begin+1)%max;
cnt--;
pthread_mutex_unlock(&mutex);
sleep(C_SLEEP);
pthread_cond_signal(&Full); //相当于条件成立后,发出成立信号的一方
}
pthread_exit((void *)0);
}
void * producer(void * pidx)//producer thread idx
{
printf("producer thread id %d\n",*((int *)pidx));
while(1)
{
pthread_mutex_lock(&mutex);
while(cnt == max)//当缓冲区满时
{
pthread_cond_wait(&Full,&mutex);
}
printf("produce %d\n",end);
end = (end+1)%max;
cnt++;
pthread_mutex_unlock(&mutex);
sleep(P_SLEEP);
pthread_cond_signal(&Empty);
}
pthread_exit((void *)0);
}

int main()
{
int i = 0;
for(i = 0; i < Num_CONSUMER; i++)
{
;int * j = (int *) malloc(sizeof(int));
*j = i;
if(pthread_create(&ctid[i],NULL,Consumer,j) != 0)
{
perror("create Consumer failed\n");
exit(1);
}
}
for(i = 0; i < Num_PRODUCER; i++)
{
int * j = (int *) malloc(sizeof(int));
*j = i;
if(pthread_create(&ptid[i],NULL,producer,j) != 0)
{
perror("create producer failed\n");
exit(1);
}
}
while(1)
{
sleep(10);
}
return 0;
}  
其中,cond 是 pthread_cond_t 类型的对象,mtx 是pthread_mutex_t类型的对象。pthread_cond_wait 在不同条件下行为不同
1. 当执行 pthread_cond_wait 时,作为一个原子操作包含以下两步:
1) 解锁互斥量 mtx
2) 阻塞进程直到其它线程调用 pthread_cond_signal 以告知 cond 可以不阻塞
2. 当执行 pthread_cond_signal(&cond) 时,作为原子操作包含以下两步:
1) 给 mtx 加锁
2)停止阻塞线程, 因而得以再次执行循环,判断条件是否满足。(注意到此时 mtx 仍然被当前线程独有,保证互斥)
赐教!