互斥量与条件变量在进程间通信的应用——生产者与消费者问题

一、互斥量和条件变量

         互斥量是一个可以处于两种状态之一的特殊变量:解锁和加锁,用于线程(进程)间互斥的进入临界区。其实互斥量是信号量的简化版本,信号量也是一种用于线程间互斥的进入临界区的特殊变量,不过信号量可以等于多个数值,可以通过数值表示临界资源的个数(累计唤醒次数),比如某个信号量mutex1=7可以用于表示缓冲区中有7个资源可以被消费者消费。mutex1=0表示缓冲区没有资源可消费,此时就要唤醒生产者来进行生产。

        条件变量用于线程的阻塞与唤醒,当某个线程对互斥量进行了加锁,但是有可能该线程缺少某些条件而不能运行,此时如果没有条件变量,则该线程就会一直等待直到它占有的CPU时间片运行完才释放CPU。而条件变量的使用可以允许线程在缺少某些条件的情况下进入睡眠状态并释放该线程持有的锁。这样操作过后,可以立马切换到其他线程进行运行,当该线程的条件到达时,即会被其他线程唤醒,继续获取锁权限然后进行运行。

        因此条件变量与互斥量是一起使用的,各自承担的任务时不一样的,互斥量是用于临界区资源访问的限制上,防止竞争条件。而条件变量是用于线程间的阻塞和唤醒操作,保证程序高效运行和线程运行的原子性。

二、生产者与消费者中的互斥量与条件变量

      假设有一个生产者A和消费者B和一个仓库C,仓库容量为1,每次生产者A在仓库为空的情况下生产一个商品放进仓库。消费者在仓库有商品的情况下,取出该商品进行消费。现在分析单独使用互斥量或则单独使用条件变量的时候会出现什么问题,由此得出为什么条件变量和互斥量通常是一起使用的。

1)仅使用互斥量

      假设此时C中容量为1,假设此时系统调度A来运行,因此A需要判断仓库是否为空,为空的情况下即给仓库上锁,然后生产一个商品放入仓库,因此A需要不停的循环判断,直到仓库为空或则时间片用完。在这个过程中,此时的B本来是可以消费仓库中的商品,但是由于系统没有分配给它CPU而等待。因此由于A不能及时将仓库有东西的消息传递给B,导致整个程序的运行效率就会下降很多。这种方式就是单纯使用互斥量会存在的忙等待的低效率问题。

2)仅使用条件变量

     在前面说到互斥量的使用时为了避免竞争条件,使得临界区的资源能够正确的被使用,那么当仅使用条件变量的时候会出现怎样的竞争条件呢? 首先,假设此时C的容量依然是1,在此时①A判断仓库是1,②A准备唤醒B然后睡眠。但是在过程①和②之间,B正好消费了仓库的商品并发送唤醒A的消息,由于A此时还没有睡眠,因此会忽略掉唤醒消息,导致消息丢失,然后A进入睡眠(A以为仓库C为1,等待B来消费),而B发送唤醒A的消息后就进入睡眠(等待A生产商品,接收A发送过来的唤醒信号)。因此这样就会出现A、B之间相互等待的情况,这就是死锁的发生。

    因此,总结上述两种情况,单独使用互斥量或则条件变量均会出现一些缺点。但是如果同时使用这两种机制,则能够很好的解决进程间通信问题。下面一节来具体分析互斥量和条件变量结合在一起时如何运作的。

三、互斥量和条件变量的运作过程

        在这里我通过程序代码的解释来阐述该过程,首先我们看一下生产者和消费者的程序代码

void *producer(void *ptr)
{
int i;
for (i=1;i<=MAX;i++)     //每次生产一个商品i
{
pthread_mutex_lock(&the_mutex); //上锁     
while (buffer!=0)
{
pthread_cond_wait(&condp,&the_mutex);
}
buffer=i;
pthread_cond_signal(&condc);
pthread_mutex_unlock(&the_mutex);  //解锁
}
pthread_exit(0);
return NULL;
}
void *consumer(void *ptr)
{
int i;
for (i=1;i<=MAX;i++)
{
pthread_mutex_lock(&the_mutex);  //取锁失败时,会调用thread_yield 将CPU放弃给另一个线程
while(buffer==0)
{
pthread_cond_wait(&condc,&the_mutex); //原子性地调用并解锁它持有的互斥量。
}
buffer=0;
pthread_cond_signal(&condp);
pthread_mutex_unlock(&the_mutex);

}
pthread_exit(0);
return NULL;
}

    生产者的运行过程是,首先给临界区仓库上锁,对应pthread_mutex_lock(&the_mutex); 然后判断仓库是否为空,如果仓库不为空,即调用pthread_cond_wait(&condp,&the_mutex); 进入睡眠,并释放锁变量,等待消费者的唤醒信号。由于生产者进入睡眠,因此系统很快就会调用消费者运行。消费者此时就可以获取到仓库的锁,然后判断仓库不为空,然后消费掉商品,接着唤醒生产者,然后释放锁变量。由于消费者发送了唤醒信号,生产者会从pthread_mutex_lock(&the_mutex)该函数返回,但是返回时并不只是接收到唤醒信号就可以,还需要重新给仓库上锁,如果上锁失败,同样会阻塞并继续等待上锁。因此这就要求消费者发送唤醒信号后立马就要执行释放锁的指令。生产者成功被唤醒和获取锁后就继续执行之前未完成的工作,生产商品i放入仓库,然后唤醒消费者,释放锁。

       从上述整个过程我们可以看到进程没有了忙等待现象,能够及时将消息传递过去,让能够执行的线程来占用资源,提高了程序的效率,而且由于使用了互斥量,任何一个线程如果没有获取锁,都无法访问临界区,保证了同一时刻只会有一个线程访问临界区。

四、程序代码验证结果

     在这里首先我引用了《现代操作系统》——Andrew S.Tanenbaum书籍上的一个生产者和消费者的代码,并略做了修改和注释,用于测试生产者和消费者之间交替运行的效果。每次生产者生产一个商品,然后消费者消费一个商品。

源代码一:

#include "stdio.h"
#include "pthread.h"
#pragma comment(lib, "pthreadVC2.lib")
#define MAX 100000000
pthread_mutex_t the_mutex;
pthread_cond_t condc,condp;
int buffer=0;
void *producer(void *ptr)
{
int i;
for (i=1;i<=MAX;i++)
{
pthread_mutex_lock(&the_mutex); //上锁
while (buffer!=0)
{
pthread_cond_wait(&condp,&the_mutex);
}
buffer=i;
printf("Producer:buffer=%d\n",buffer);
pthread_cond_signal(&condc);
pthread_mutex_unlock(&the_mutex);  //解锁
}
pthread_exit(0);
return NULL;
}
void *consumer(void *ptr)
{
int i;
for (i=1;i<=MAX;i++)
{
pthread_mutex_lock(&the_mutex);  //取锁失败时,会调用thread_yield 将CPU放弃给另一个线程
while(buffer==0)
{
pthread_cond_wait(&condc,&the_mutex); //原子性地调用并解锁它持有的互斥量。
}
buffer=0;
printf("Consumer:buffer=%d\n",buffer);
pthread_cond_signal(&condp);
pthread_mutex_unlock(&the_mutex);

}
pthread_exit(0);
return NULL;
}
int main(int argc,char **argv)
{
pthread_t pro,con;
pthread_mutex_init(&the_mutex,0); //锁的初始化
pthread_cond_init(&condc,0);
pthread_cond_init(&condp,0);
pthread_create(&con,0,consumer,0);
pthread_create(&pro,0,producer,0);
pthread_join(pro,0);
pthread_join(con,0);
pthread_cond_destroy(&condc);
pthread_cond_destroy(&condp); //条件变量销毁,没有线程等待该条件变量时候,才能销毁
pthread_mutex_destroy(&the_mutex);  //锁的销毁,
return 0;
}


上图中可以看到消费者和生产者的交替运行结果。为了测试在第三节中所讲的pthread_cond_wait(&condp,&the_mutex)函数返回,是需要重新给仓库上锁,如果上锁失败,也会阻塞的结论,我对上述代码一做了一点修改,并增加了输出信息。我让生产者在发送唤醒消费者的消息后,不释放锁,而是睡眠3秒钟,然后再释放锁。由于生产者发送了唤醒信号,消费者会重新上锁,由于上锁失败,消费者会阻塞,并不能立马消费掉商品,而要等待生产者释放锁,才能顺利消费商品。

源代码二:

#include "stdio.h"
#include "pthread.h"
#include "windows.h"
#pragma comment(lib, "pthreadVC2.lib")
pthread_mutex_t the_mutex;
pthread_cond_t condc,condp;
int buffer=0;
void *producer(void *ptr)
{
int i;
for (i=1;i<=1;i++)
{
pthread_mutex_lock(&the_mutex); //上锁
while (buffer!=0)
{
pthread_cond_wait(&condp,&the_mutex);
}
buffer=i;
printf("Producer:buffer=%d\n",i);
pthread_cond_signal(&condc);
printf("Producer:after signal consumer ,sleep 3s\n",i);
Sleep(3000);
printf("Producer:Now, unlock the mutex\n");
pthread_mutex_unlock(&the_mutex);  //解锁
}
pthread_exit(0);
return NULL;
}
void *consumer(void *ptr)
{
int i;
for (i=1;i<=1;i++)
{
printf("\nConsumer:Wait for mutex\n");
pthread_mutex_lock(&the_mutex);  //取锁失败时,会调用thread_yield 将CPU放弃给另一个线程
printf("Consumer:Get the mutex\n");
while(buffer==0)
{
printf("Consumer:block\n");
pthread_cond_wait(&condc,&the_mutex); //原子性地调用并解锁它持有的互斥量。
printf("Consumer: signal and get the mutex\n");
}
buffer=0;
printf("Consumer:buffer=%d",0);
Sleep(20000);
pthread_cond_signal(&condp);
pthread_mutex_unlock(&the_mutex);

}
pthread_exit(0);
return NULL;
}
int main(int argc,char **argv)
{
pthread_t pro,con;
pthread_mutex_init(&the_mutex,0); //锁的初始化
pthread_cond_init(&condc,0);
pthread_cond_init(&condp,0);
pthread_create(&con,0,producer,0);
pthread_create(&pro,0,consumer,0);
pthread_join(pro,0);
pthread_join(con,0);
pthread_cond_destroy(&condc);
pthread_cond_destroy(&condp); //条件变量销毁,没有线程等待该条件变量时候,才能销毁
pthread_mutex_destroy(&the_mutex);  //锁的销毁,
return 0;
}


    从上图结果可以看到,消费者进行了两次上锁,在生产者唤醒消费者后,消费者不能立马消费商品,等到生产者睡眠3s后释放锁,才能再次上锁,然后消费商品。这就验证了pthread_cond_wait(&condp,&the_mutex)函数的返回除了接收唤醒信号,还要重新对临界区上锁。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值