pthread_mutex_t 和 pthread_cond_t原理、使用、注意事项

1.原理
假设有两个线程同时访问一个全局变量 n,这个全局变量的初始值等于0。

Int  n = 0 ;

         消费者线程 A 进入临界区,访问 n,A 必须等到 n 大于 0 才能接着往下执行,如果 n== 0,那么 A 将一直等待。

         还有一个生产者线程 B,B 进入临界区,修改 n 的值,使得 n >0,当 n > 0 时,B 通知等待 n > 0 的消费者线程A。A 被 B 通知之后就可以接着往下执行了。

 以上情况造成死锁:

         当 A 进入临界区时,其他线程不能进入临界区,意味着 B 没有机会去修改 n, n 的值一直为 0,不满足A 继续执行的条件(n > 0),A 只能一直等待。

         消费者进程拿到互斥锁 --> 进入临界区 --> 发现共享资源 n 不满足继续执行的条件(n> 0) --> 等待 n > 0

         消费者进程占有互斥锁 --> 生产者进程无法进入临界区 --> 无法修改 n 的值 --> 生产者等待消费者释放互斥锁

         解决死锁的方案就是采用条件变量。

         通常情况下,对共享资源(比如 n)保护要用到锁操作,当一个进程进入临界区时会拿到互斥锁(lock 操作),然后其他进程拿不到互斥锁,也就无法进入临界区,因此当进程进入临界区,发现共享资源不满足继续向下执行的条件(n > 0)时,就应该释放锁,让其他进程修改共享资源,以满足自己所需的执行条件。

消费者进入临界区 --> 共享变量不满足继续向下执行的条件 --> 消费者等待在条件变量 --> 释放互斥锁 --> 生产者进入临界区 --> 修改条件变量 --> 生产者通知消费者:现在有多的资源了,快来使用 --> 消费者再次拿互斥锁 --> 消费资源 --> 释放互斥锁。如果有多个消费者进程等待在条件变量上,就可以形成等待队列。

生产者和消费者模型中互斥锁和条件变量的使用流程图如下,其中蓝色代表消费者的执行流,红色是生产者的执行流。

2.使用方法

         条件变量的使用主要有以下五个函数:

/* 初始化一个条件变量 */
int pthread_cond_init (pthread_cond_t* cond, pthread_condattr_t *cond_attr);
 
/* 销毁一个条件变量 */
int pthread_cond_destroy(pthread_cond_t* cond);
 
/* 令一个消费者等待在条件变量上 */
int pthread_cond_destroy(pthread_cond_t* cond);
 
/* 生产者通知等待在条件变量上的消费者 */
int pthread_cond_signal(pthread_cond_t* cond);
 
/* 生产者向消费者广播消息 */
int pthread_cond_broadcast(pthread_cond_t* cond);

消费者等待条件的伪代码:

pthread_mutex_lock(&mutex); // 拿到互斥锁,进入临界区
while( 条件为假)
	pthread_cond_wait(cond, mutex); // 令进程等待在条件变量上
修改条件
pthread_mutex_unlock(&mutex); // 释放互斥锁

生产者通知消费者的伪代码:

pthread_mutex_lock(&mutex); // 拿到互斥锁,进入临界区
设置条件为真
pthread_cond_signal(cond); // 通知等待在条件变量上的消费者
pthread_mutex_unlock(&mutex); // 释放互斥锁

以下是示例程序,演示了互斥锁和条件变量配合使用方法,由于是在Linux下写的程序,所以注释全是英文的。

condition_test.c:

/***************************************************************
*  Copyright (C) 2016 chengonghao
*  All rights reserved.
*
*  chengonghao@yeah.net
***************************************************************/
#include <unistd.h>
#include <pthread.h>
 
#define CONSUMERS_COUNT 2	
#define PRODUCERS_COUNT 1
 
pthread_mutex_t g_mutex ;
pthread_cond_t g_cond ;
 
pthread_t g_thread[CONSUMERS_COUNT + PRODUCERS_COUNT] ;
int share_variable = 0 ;// this is the share variable, shared by consumer and producer
 
void* consumer( void* arg )
{
   int num = (int)arg ;
   while ( 1 ) 
   {
      /******* critical section begin *******/
      pthread_mutex_lock( &g_mutex ) ;
 
      // if share_variable == 0, means consumer shell stop here
      while ( share_variable == 0 )
      {
         printf( "consumer %d begin wait a condition...\n", num ) ;
         // put a thread blocked ont a condition variable( here is g_cond),
         // and unlock the mutex( here is g_mutex )
         pthread_cond_wait( &g_cond, &g_mutex ) ;
      }
      // here means n != 0 and consumer can goes on
      // consumer consumed shared variable, so the number of shared variable shell minus
      printf( "consumer %d end wait a condition...\n", num ) ;
      printf( "consumer %d begin consume product\n", num ) ;
      -- share_variable ;
 
      pthread_mutex_unlock( &g_mutex ) ;
      /******** critial section end *********/
      sleep( 1 ) ;
   }
   
   return NULL ;
}
 
void* producer( void* arg )
{
   int num = (int)arg ;
   while ( 1 )
   {
      /******* critical section begin *******/
      pthread_mutex_lock( &g_mutex ) ;
 
      // produce a shared variable
      printf( "producer %d begin produce product...\n", num ) ;
      ++ share_variable ;
      printf( "producer %d end produce product...\n", num ) ;
      // unblock threads blocked on a condition variable( here is g_cond )
      pthread_cond_signal( &g_cond ) ;
      printf( "producer %d notified consumer by condition variable...\n", num ) ;
      pthread_mutex_unlock( &g_mutex ) ;
 
      /******** critial section end *********/
      sleep( 5 ) ;
   }
   
   return 1 ;
}
 
 
int main( void )
{
   // initiate mutex
   pthread_mutex_init( &g_mutex, NULL ) ;
   // initiate condition
   pthread_cond_init( &g_cond, NULL ) ;
 
   // initiate consumer threads
   for ( int i = 0; i < CONSUMERS_COUNT; ++ i )
   {
      pthread_create( &g_thread[i], NULL, consumer, (void*)i ) ;
   }
   sleep( 1 ) ;
   // initiate producer threads
   for ( int i = 0; i < PRODUCERS_COUNT; ++ i )
   {
      pthread_create( &g_thread[i], NULL, producer, (void*)i ) ;
   }
   for ( int i = 0; i < CONSUMERS_COUNT + PRODUCERS_COUNT; ++ i ) 
   {
      pthread_join( g_thread[i], NULL ) ;
   }
 
   pthread_mutex_destroy( &g_mutex ) ;
   pthread_cond_destroy( &g_cond ) ;
}

使用效率问题:

pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。

但使用pthread_cond_signal不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。

另外,互斥量的作用一般是用于对某个资源进行互斥性的存取,很多时候是用来保证操作是一个原子性的操作,是不可中断的。

用法:

pthread_cond_wait必须放在pthread_mutex_lock和pthread_mutex_unlock之间,因为他要根据共享变量的状态来决定是否要等待,而为了不永远等待下去所以必须要在lock/unlock队中

共享变量的状态改变必须遵守lock/unlock的规则

pthread_cond_signal即可以放在pthread_mutex_lock和pthread_mutex_unlock之间,也可以放在pthread_mutex_lock和pthread_mutex_unlock之后,但是各有各缺点。

之间:

pthread_mutex_lock

xxxxxxx

pthread_cond_signal

pthread_mutex_unlock

缺点:在某下线程的实现中,会造成等待线程从内核中唤醒(由于cond_signal)然后又回到内核空间(因为cond_wait返回后会有原子加锁的行为),所以一来一回会有性能的问题。

在code review中,我会发现很多人喜欢在pthread_mutex_lock()和pthread_mutex_unlock(()之间调用 pthread_cond_signal或者pthread_cond_broadcast函数,从逻辑上来说,这种使用方法是完全正确的。但是在多线程环境中,这种使用方法可能是低效的。posix1标准说,pthread_cond_signal与pthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是说,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。这里举个例子:
       我们假设系统中有线程1和线程2,他们都想获取mutex后处理共享数据,再释放mutex。请看这种序列:
       1)线程1获取mutex,在进行数据处理的时候,线程2也想获取mutex,但是此时被线程1所占用,线程2进入休眠,等待mutex被释放。
       2)线程1做完数据处理后,调用pthread_cond_signal()唤醒等待队列中某个线程,在本例中也就是线程2。线程1在调用pthread_mutex_unlock()前,因为系统调度的原因,线程2获取使用CPU的权利,那么它就想要开始处理数据,但是在开始处理之前,mutex必须被获取,很遗憾,线程1正在使用mutex,所以线程2被迫再次进入休眠。
       3)然后就是线程1执行pthread_mutex_unlock()后,线程2方能被再次唤醒。
       从这里看,使用的效率是比较低的,如果再多线程环境中,这种情况频繁发生的话,是一件比较痛苦的事情。

但是在LinuxThreads或者NPTL里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。

所以在Linux中推荐使用这种模式。

之后:

pthread_mutex_lock

xxxxxxx

pthread_mutex_unlock

pthread_cond_signal

优点:不会出现之前说的那个潜在的性能损耗,因为在signal之前就已经释放锁了

缺点:如果unlock和signal之前,有个低优先级的线程正在mutex上等待的话,那么这个低优先级的线程就会抢占高优先级的线程(cond_wait的线程),而这在上面的放中间的模式下是不会出现的。

所以,在Linux下最好pthread_cond_signal放中间,但从编程规则上说,其他两种都可以

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值