Linux——多线程(三):线程同步,条件变量


1. 线程同步

作用:在多个执行流访问临界资源的时候是合理访问的

模拟一个如下场景,假设,有一个线程A,一个线程B,一个碗,线程A负责吃碗里的面,线程B负责给碗里做面,此时我们用如下代码进行模拟:

    1 #include<stdio.h>                                                                                                          
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 
    5 int g_bowl = 1;
    6 
    7 pthread_mutex_t My_lock;
    8 
    9 //Eat
   10 void* MythreadA(void* arg)
   11 {
   12     while(1)
   13     {
   14         pthread_mutex_lock(&My_lock);
   15 
   16         printf("i eat %d,i am %p\n",g_bowl,pthread_self());
   17 
   18         g_bowl--;
   19 
   20         pthread_mutex_unlock(&My_lock);
   21 
   22     }
   23     return NULL;
   24 }
   25 
   26 //Make
   27 void* MythreadB(void* arg)
   28 {
   29     while(1)
   30     {
   31         pthread_mutex_lock(&My_lock);
   32 
   33         g_bowl++;
   34 
   35         printf("i make %d,i am %p\n",g_bowl,pthread_self());
   36 
   37 
   38         pthread_mutex_unlock(&My_lock);
   39 
   40     }
   41     return NULL;
   42 }
   43 
   44 int main()
   45 {
   46     //锁初始化
   47     pthread_mutex_init(&My_lock,NULL);                                                                                     
   48 
   49     pthread_t tid_Eat,tid_Make;
   50 
   51     //创建两个线程
   52     int ret = pthread_create(&tid_Eat,NULL,MythreadA,NULL);
   53     if(ret < 0)  
   54     {
   55         perror("pthread_create");                           
   56         return 0;
   57     }
   58     pthread_create(&tid_Make,NULL,MythreadB,NULL);
   59     if(ret < 0)
   60     {
   61         perror("pthread_create");
   62         return 0;
   63     }
   64           
   65  
   66     //线程等待
   67     pthread_join(tid_Eat,NULL);
   68     pthread_join(tid_Make,NULL);
   69                                
   70     pthread_mutex_destroy(&My_lock);
   71     return 0;     
   72 }                                                                        

此时,我们将程序跑起来,可能会看到bowl已经减为负数,也有可能看到bowl加为超过1的正数,为什么会出现上述情况呢?因为线程A负责吃面,线程B负责做面,当线程B拿到CPU资源时,它可能持续往碗里做面,就会出现bowl>1的情况,当线程B拿到CPU资源时,它可能持续吃面,就会出现bowl<0的情况,如下图所示:
在这里插入图片描述
而线程同步就是解决如上问题的,同步,限制线程A什么时候吃面,线程B什么时候做面

2. 条件变量

条件变量 = PCB等待队列 + 一堆接口

2.1 PCB等待队列

PCB等待队列:当线程发现资源不可用的时候,调用条件变量接口,将自己放到PCB等待队列中,等待被唤醒

图解如下:
在这里插入图片描述

2.2 条件变量的接口

条件变量的类型:pthread_cond_t

2.2.1 初始化

静态初始化:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

动态初始化

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

  • cond:待要初始化的“条件变量”的变量,一般情况下,传递一个pthread_cond_t类型变量的地址
  • attr:一般情况下给NULL,采用默认属性

2.2.2 等待接口

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

  • cond:待要初始化的“条件变量”的变量,一般情况下,传递一个pthread_cond_t类型变量的地址
  • mutex:互斥锁
  • 作用:如果一个执行流调用了该接口,就会将执行流对应的PCB放到参数cond的PCB等待队列当中

2.2.3 唤醒接口

int pthread_cond_signal(pthread_cond_t *cond);

  • 作用:通知(唤醒)PCB等待队列当中的线程,如果被通知(唤醒)的线程接收到了,则从PCB等待队列当中出队操作,正常执行代码
  • 注意:此接口至少唤醒一个PCB等待队列当中的线程

int pthread_cond_broadcast(pthread_cond_t *cond);

  • 注意:此接口唤醒所有PCB等待队列当中的线程

2.2.4 销毁接口

int pthread_cond_destroy(pthread_cond_t *cond);

2.3 代码验证

给上文模拟的吃面做面代码加上条件变量约束,线程A先判断碗里是否有面,若有面就吃面,吃完面,通知线程B做面,若碗里没有面,则线程A进行等待。线程B先判断碗里是否有面,若有面,则线程B进行等待,若没有面,则线程B做面,做完面后通知线程A吃面,改进后的代码如下:

    1 #include<stdio.h>                                                                                                          
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 
    5 #define THREAD_NUM 1
    6 int g_bowl = 1;
    7 
    8 pthread_mutex_t My_lock;
    9 pthread_cond_t g_cond;
   10 
   11 //Eat
   12 void* MythreadA(void* arg)
   13 {
   14     while(1)
   15     {
   16         pthread_mutex_lock(&My_lock);
   17 
   18         //判断是否能吃面
   19         if(g_bowl < 1)
   20         {
   21             //等待
   22             pthread_cond_wait(&g_cond,&My_lock);
   23         }
   24         printf("i eat %d,i am %p\n",g_bowl,pthread_self());
   25 
   26         g_bowl--;
   27 
   28         pthread_mutex_unlock(&My_lock);
   29         //通知做面
   30         pthread_cond_signal(&g_cond);
   31 
   32     }
   33     return NULL;
   34 }
   35 
   36 //Make
   37 void* MythreadB(void* arg)
   38 {
   39     while(1)
   40     {
   41         pthread_mutex_lock(&My_lock);
   42 
   43         //判断是否需要做面
   44         if(g_bowl > 0)
   45         {
   46             //等待
   47             pthread_cond_wait(&g_cond,&My_lock);                                                                           
   48         }
   49 
   50         g_bowl++;
   51         printf("i make %d,i am %p\n",g_bowl,pthread_self());
   52 
   53 
   54         pthread_mutex_unlock(&My_lock);
   55         //通知吃面
   56         pthread_cond_signal(&g_cond);                                                                                      
   57 
   58     }
   59     return NULL;
   60 }
   61 
   62 int main()
   63 {
   64     //锁初始化
   65     pthread_mutex_init(&My_lock,NULL);
   66     //初始化条件变量
   67     pthread_cond_init(&g_cond,NULL);
   68 
   69     pthread_t tid_Eat[THREAD_NUM],tid_Make[THREAD_NUM];
   70 
   71     //创建线程
   72     for(int i = 0;i < THREAD_NUM;i++)
   73     {
   74         int ret = pthread_create(&tid_Eat[i],NULL,MythreadA,NULL);
   75         if(ret < 0)
   76         {
   77             perror("pthread_create");  
   78             return 0;
   79         }
   80         pthread_create(&tid_Make[i],NULL,MythreadB,NULL);
   81         if(ret < 0)
   82         {       
   83             perror("pthread_create");
   84             return 0;
   85         } 
   86  
   87     }         
   88                                       
   89     //线程等待      
   90     for(int i = 0;i < THREAD_NUM;i++)
   91     {
   92         pthread_join(tid_Eat[i],NULL);                 
   93         pthread_join(tid_Make[i],NULL);
   94     }         
   95                                      
   96     pthread_mutex_destroy(&My_lock);
   97     pthread_cond_destroy(&g_cond);                                
   98     return 0;      
   99 }                                                                 

让程序跑起来,我们可以看到线程A吃一碗面,线程B做一碗面,如下图所示:

在这里插入图片描述
如上程序虽然看着符合我们的预期,实则,当我们将吃面和做面的线程分别创建两个,就会看到程序依然有错,如下图所示:
在这里插入图片描述
在这里插入图片描述
对如上运行错误进行分析,如下:

假设,碗里有面,此时make1拿到了锁,则make1判断碗里有面后将自己放入PCB等待队列中进行等待,然后释放互斥锁,假设此时,eat1拿到了互斥锁,然后eat1吃掉碗里的面,然后释放锁并通知PCB等待队列,此时make1已经出队,假设此时make2拿到了锁,并做了一碗面,然后释放锁,然后make1又拿到了锁,而此时make1将要执行的是pthread_cond_wait函数之后那条命令,则make1跳过了判断碗里是否有面,直接往碗里做面,此时bowl的值就由1变为了2

图解如下:
在这里插入图片描述
所以要解决此问题,只需循环判断,将if改为while即可,此时将程序跑起来,结果如下:
在这里插入图片描述
出现这种情况,说明是极端情况,所有的执行流PCB都进入PCB等待队列中进行等待,所以程序就不再往下执行,要想解决此问题,给吃面的线程和做面的线程各配一个单独的条件变量,只需两个等待队列,吃面的人通知做面的等待队列,做面的人通知吃面的等待队列

图解如下:
在这里插入图片描述
代码如下:

    1 #include<stdio.h>
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 
    5 #define THREAD_NUM 2
    6 int g_bowl = 1;
    7 
    8 pthread_mutex_t My_lock;
    9 //eat的条件变量
   10 pthread_cond_t g_cond;
   11 //make的条件变量                                                                                                           
   12 pthread_cond_t g_make_cond;
   13                           
   14 //Eat                                           
   15 void* MythreadA(void* arg)
   16 {                                                          
   17     while(1)                         
   18     {                                 
   19         pthread_mutex_lock(&My_lock);  
   20                                        
   21         //判断是否能吃面 
   22         while(g_bowl < 1)                 
   23         {                                       
   24             //等待                              
   25             pthread_cond_wait(&g_cond,&My_lock);           
   26         }                                                  
   27         printf("i eat %d,i am %p\n",g_bowl,pthread_self());
   28 
   29         g_bowl--;
   30 
   31         pthread_mutex_unlock(&My_lock);
   32         //通知做面
   33         pthread_cond_signal(&g_make_cond);
   34 
   35     }
   36     return NULL;
   37 }
   38 
   39 //Make
   40 void* MythreadB(void* arg)
   41 {
   42     while(1)
   43     {
   44         pthread_mutex_lock(&My_lock);
   45 
   46         //判断是否需要做面
   47         while(g_bowl > 0)                                                                                                  
   48         {
   49             //等待
   50             pthread_cond_wait(&g_make_cond,&My_lock);
   51         }
   52 
   53         g_bowl++;
   54         printf("i make %d,i am %p\n",g_bowl,pthread_self());
   55 
   56 
   57         pthread_mutex_unlock(&My_lock);
   58         //通知吃面                                                                                                         
   59         pthread_cond_signal(&g_cond);
   60 
   61     }
   62     return NULL;
   63 }
   64 
   65 int main()
   66 {
   67     //锁初始化
   68     pthread_mutex_init(&My_lock,NULL);
   69     //初始化条件变量
   70     pthread_cond_init(&g_cond,NULL);
   71     pthread_cond_init(&g_make_cond,NULL);
   72 
   73     pthread_t tid_Eat[THREAD_NUM],tid_Make[THREAD_NUM];
   74 
   75     //创建线程
   76     for(int i = 0;i < THREAD_NUM;i++)
   77     {
   78         int ret = pthread_create(&tid_Eat[i],NULL,MythreadA,NULL);
   79         if(ret < 0)
   80         {
   81             perror("pthread_create");
   82             return 0;
   83         }
   84         pthread_create(&tid_Make[i],NULL,MythreadB,NULL);
   85         if(ret < 0)
   86         {
   87             perror("pthread_create");
   88             return 0;
   89         }
   90 
   91     }
   92 
   93     //线程等待
   94     for(int i = 0;i < THREAD_NUM;i++)
   95     {
   96         pthread_join(tid_Eat[i],NULL);
   97         pthread_join(tid_Make[i],NULL);                                                                                    
   98     }
   99 
  100     pthread_mutex_destroy(&My_lock);
  101     pthread_cond_destroy(&g_cond);
  102     pthread_cond_destroy(&g_make_cond);
  103     return 0;
  104 }                            

程序跑起来如下:
在这里插入图片描述

3.条件变量关于等待接口的几个问题

【问题1】条件变量的等待接口参数为什么需要互斥锁?

由于需要在pthread_cond_wait函数内部进行解锁,解锁之后,其他的执行流才能获取到这把互斥锁,所以,需要传入互斥锁,否则,如果在调用pthread_cond_wait线程在进行等待的时候,不释放互斥锁,其他线程也不会获取到互斥锁,程序就没有办法继续运行了

图解如下:
在这里插入图片描述

【问题2】在调用条件变量等待接口的时候,pthread_cond_wait函数的实现逻辑是什么?

(1)先将执行流的PCB放到参数cond的PCB等待队列
(2)释放互斥锁
(3)等待被唤醒

如下用反例来证明

  假设,做面的线程判断碗里有面,要进行等待,若它先释放互斥锁,在它释放完互斥锁还未进入等待队列之前,可能吃面的线程拿到了互斥锁,并吃完了面,通知PCB等待队列,而此时做面的线程,还未进入PCB等待队列,PCB等待队列此事为空,吃面的线程又拿到锁去吃面,此时,吃面的线程判断碗里没有面,则将自己放入PCB等待队列中进行等待,而此时做面的线程也在PCB等待队列中进行等待,所以不能先释放互斥锁

反例图解如下:
在这里插入图片描述
所以,pthread_cond_wait函数的实现逻辑是,先将执行流的PCB放到参数cond的PCB等待队列,然后再释放互斥锁

【问题3】如果一个线程在等待的时候,被唤醒之后,需要做什么事情?

(1)移动出PCB等待队列
(2)抢互斥锁

  • 抢到了:pthread_cond_wait函数返回了
  • 没抢到:pthread_cond_wait函数没有返回,等待抢锁
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值