• 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
    " , 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等待。
    ", id);
                 pthread_cond_wait(&n_empty, &mutex);
                 printf("有空位,数量为%d
    ", empty_num);
             }
             printf("生产线程%d生产
    ", id);
             ++g_num;
             --empty_num;
             printf("生产品%d完成
    ", 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等待。。
    ", id);
                 pthread_cond_wait(&cond, &mutex);
                 printf("第%d产品到了!!
    ", g_num);
             }
             printf("消费线程%d消费 产品%d
    ", id , g_num);
             --g_num;
             ++empty_num;
             sleep(rand()%2);
             printf("消费线程%d消费完成
    ", 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),这样生产、消费得以进行。如下:

  • 相关阅读:
    绑定方法和非绑定方法
    property属性
    面向对象的三大特征之一:封装
    asp:GridView控件的使用
    javaWeb中struts开发——Logic标签
    javaWeb中struts开发——Bean标签
    大话数据结构(十二)java程序——KMP算法及改进的KMP算法实现
    大话数据结构(十一)java程序——串
    大话数据结构(七)——单链表的整表创建与删除
    大话数据结构(十)java程序——队列
  • 原文地址:https://www.cnblogs.com/tp-16b/p/9077438.html
Copyright © 2020-2023  润新知