Linux——多线程下解决生产消费者模型

我们学习了操作系统,想必对生产消费者问题都不陌生。作为同步互斥问题的一个经典案例,生产消费者模型其实是解决实际问题的基础模型,解决很多的实际问题都会依赖于它。而此模型要解决最大的问题便是同步与互斥。而通常呢,在多进程的环境下我们一般是是用信号量来解决(可以戳这里看看);在多线程的情况,则会用到两个东西:  互斥量和条件变量通常用它们两个来实现线程间通信,以此来解决多线程下的同步和互斥问题。不过在具体实现生产消费模型前,为了更好理解当中的处理原理,还是先来回顾一下一些线程间通信的相关知识。

 

 互斥问题


大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

 // 操作共享变量会有问题的售票系统代码
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <pthread.h>
 int ticket = 100;
 void *route( void *arg) 
 {
     char id = *(char*)arg;
     while (  1 )  { 
         if (  ticket > 0 )  { 
             usleep( 1000) ;
             printf( " thread %c sells ticket:%d\n" , id, ticket) ;
             ticket--;
         }  else { 
             break;
         } 
     }
 }  
 int main(void) 
 {  
     pthread_t t1, t2, t3, t4;
     char a1=1,a2=2,a3=3,a4=4;
     pthread_create( &t1, NULL, route, &a1);
     pthread_create( &t2, NULL, route, &a2);
     pthread_create( &t3, NULL, route, &a3);
     pthread_create( &t4, NULL, route, &a4);
     pthread_join( t1, NULL) ;
     pthread_join( t2, NULL) ;
     pthread_join( t3, NULL) ;
     pthread_join( t4, NULL) ;
 }
 //一次执行结果:
 thread 4 sells ticket:100
 ...
 thread 4 sells ticket:1
 thread 2 sells ticket:0
 thread 1 sells ticket:-1
 thread 3 sells ticket:-2

为什么无法获得正确结果?

·if 语句判断条件为真以后,代码可以并发的切换到其他线程;usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
·ticket--操作本身就不是一个原子操作

如果取出 “ticket--”部分的汇编代码

objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34
<ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34

ticket--操作并不是原⼦子操作,而是对应三条汇编指令:
  load:将共享变量ticket从内存加载到寄存器中
  update: 更新寄存器里面的值,执行-1操作
  store:将新值,从寄存器写回共享变量ticket的内存地址


要解决以上问题,需要做到三点:


1.代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

2.如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

3.如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

Linux下用互斥量就做到了以上3点,它本质上其实就是一把锁。

 

互斥量


互斥量使用一般是以下几个步骤:

1.定义互斥量(mutex): pthread_mutex_t  mutex;

2.初始化:

  ①静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 

  ②动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
    mutex:要初始化的互斥量
    attr:如果不设置线程属性的话填NULL

 

3.上锁:pthread_mutex_lock(&mutex)   如果是1,值0,返回;  如果是0,便阻塞

调⽤用pthread_ lock 时,可能会遇到以下情况:

互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,
但没有竞争到互斥量,那么pthread_ lock调⽤用会陷入阻塞,等待互斥量解锁。

4.解锁: pthread_mutex_unlock(&mutex)   置为1,返回

5.销毁:pthread_mutex_destroy(&mutex)    

销毁互斥量需要注意:使⽤用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
不要销毁⼀一个已经加锁的互斥量已经销毁的互斥量,要确保后⾯面不会有线程再尝试加锁

 

 

自旋锁

互斥锁是当阻塞在pthread_mutex_lock时,放弃CPU,好让别人使用CPU。自旋锁阻塞在spin_lock时不会阻塞CPU,不断对CPU询问。(实时系统中应用比较多,要求对锁进行较快响应)它使用形式与互斥量类似,不再赘述。

1.定义自旋锁: pthread_spinlock_t spin

2.初始化自旋锁:pthread_spin_intt(pthread_spinlock_t *s, int s)

3.上锁:int pthread_spin_lock(pthread_spinlock_t *lock)

4.解锁:int pthread_spin_lock(pthread_spinlock_t *lock)

5.销毁:int pthread_spin_lock(pthread_spinlock_t *lock)

 

读写锁

在编写多线程的时候,有⼀一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极⼤大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。读写锁本质上是一种自旋锁[长时间等人和短时间等人的例子]

·注意:读共享,写排他,写优先级高

它处理方式和前面互斥量类似,就不在赘述。

1.定义:pthread_rwlock_t lock

2.初始化  pthread_rwlock_init(&lock, NULL)

3.上锁:pthread_rwlock_rdlock(&lock)     pthread_rwlock_wrlock(&lock)

4.解锁:pthread_rwlock_unlock(&lock)

5.销毁:pthread_rwlock_destroy(&lock)

 

 

 

条件变量


生产者消费问题要解决另一个问题就同步的问题。当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况下就需要用到条件变量了。

使用步骤如下:

1.定义条件变量:

pthread_cond_t cond;
pthread_mutex_t mutex

2.初始化条件变量:

pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *restrict attr)
  参数:
         cond:要初始化的条件变量 
         attr:填NULL(用于设置线程属性)        

3.等待条件:

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
 参数:
    cond:要在这个条件变量上等待
    mutex:互斥量,后⾯面详细解释

注意这里wait函数需要互斥量(后面解释)。如果在锁环境下,此处互斥量形同虚设。在锁环境下,会将mutex解锁; wait返回时,将mutex锁制成原来状态

4.使条件满足 :

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

5.销毁条件变量:

int pthread_cond_destroy(pthread_cond_t *cond)

 

为什么pthread_ cond_ wait 需要互斥量?

①条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
②条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

             

 

按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了?但是这样也会有问题

// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
    pthread_mutex_unlock(&mutex);
    //解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
    pthread_cond_wait(&cond);
    pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

·由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_ cond_wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait。所以解锁和等待必须是一个原子操作。
·int pthread_cond_wait; 进入该函数后,会去看条件量是否为0?等于0,就把互斥量变成1,直到cond_wait返回时,把条件量改成1,同时将互斥量恢复成原样。

 

所以正确是条件变量的使用规范是这样的:(这里以生产消费问题为例 简单的实现一下同步,使得消费者需要在有产品的情况下才可进行消费。)

                  

条件变量使用范例即:

·等待条件:

pthread_mutex_lock(&mutex);
while (条件为假)
    pthread_cond_wait(&cond, &mutex);
        //pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mutex,
        //然后阻塞在等待队列里休眠,直到再次被唤醒 
        //(大多数情况下是等待的条件成立而被唤醒,唤醒后,
        //该进程会进行pthread_mutex_lock(&mutex)先锁定,然后再读取资源
修改条件
pthread_mutex_unlock(&mutex);   

·给条件发送信号代码

pthread_mutex_lock(&mutex);
//设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

 

 

多线程下的生产消费者问题


好,这下终于把准备工作做好了,结合线程的基本操作,多线程下的生产消费者我们也就不难实现出来了。如下:

 /*************************************************************************
   > File Name: pc.c
   > Author: tp
   > Mail: 
   > Created Time: Sun 27 May 2018 06:28:33 PM CST
  ************************************************************************/
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <pthread.h>
 
 #define PRO_NUM 3   //生产线程数量 
 #define CON_NUM 0   //消费线程数量
 pthread_cond_t cond;
 pthread_cond_t n_empty;
 pthread_mutex_t mutex;
 
 int g_num = 0;      //产品数量
 int empty_num = 3;  //生产的空位数量
 
 //productor
 void* pro_route(void* arg)
 {
     int id =*(int*)arg;
     free(arg);
 
     while(1)
     {
         pthread_mutex_lock(&mutex);
         while(empty_num <= 0)
         {
             printf("生产线程%d等待。\n", id);
             pthread_cond_wait(&n_empty, &mutex);
             printf("有空位,数量为%d\n", empty_num);
         }
         printf("生产线程%d生产\n", id);
         ++g_num;
         --empty_num;
         printf("生产品%d完成\n", g_num);
         sleep(rand()%3);
         pthread_cond_signal(&cond);
         pthread_mutex_unlock(&mutex);
         sleep(rand()%3);
     }
 }
 //consumer
 void *con_route(void* arg)
 {
     int id =*(int*)arg;
     free( arg);
 
     while(1)
     {
         pthread_mutex_lock(&mutex);
         while(g_num <= 0)
         {
             printf("消费线程%d等待。。\n", id);
             pthread_cond_wait(&cond, &mutex);
             printf("第%d产品到了!!\n", g_num);
         }
         printf("消费线程%d消费 产品%d\n", id , g_num);
         --g_num;
         ++empty_num;
         sleep(rand()%2);
         printf("消费线程%d消费完成\n", id);
         pthread_cond_signal(&n_empty);
         pthread_mutex_unlock(&mutex);
         sleep(rand()%3);
     }
 }
 int main( )
 {
     srand(getpid());
 
     pthread_t tids[PRO_NUM + CON_NUM];
     //互斥量,条件变量初始化
     pthread_mutex_init(&mutex, NULL);
     pthread_cond_init(&cond, NULL);   //条件变量1
     pthread_cond_init(&n_empty, NULL);//条件变量2
 
     //创建生产者线程
     for(int i =0; i< PRO_NUM; ++i)
     {
         int* p = (int*)malloc(sizeof(int)); //传入参数相当作线程编号
         *p = i;
         pthread_create(&tids[i], NULL, pro_route, p);
     }
     //创建消费者线程
     for(int i =0; i< CON_NUM; ++i)
     {
         int* p = (int*)malloc(sizeof(int)); //消费线程编号
         *p = i;
         pthread_create(&tids[i], NULL, con_route, p);
     }
     for(int i =0; i< PRO_NUM + CON_NUM; ++i) //回收线程
     {
         pthread_join(tids[i], NULL);
     }
     //互斥量,条件变量销毁
     pthread_mutex_destroy(&mutex);
     pthread_cond_destroy(&cond);
     pthread_cond_destroy(&n_empty);
 
     return 0;
 }

 结果:

 当我们只有生产者生产时,此时生产的空位生产满了之后便会阻塞,如下:

当去添加两个消费线程时(将CON_NUM改为2),这样生产、消费得以进行。如下:

 

posted @ 2018-05-31 17:58  tp_16b  阅读(2028)  评论(0编辑  收藏  举报