线程满汉全席之线程安全>>互斥锁、条件变量

目录

概念:

线程安全:

线程不安全的原理:

线程安全:

互斥锁:

互斥锁原理:

同步:

线程不安全案例:

条件变量的原理:

注:如果本篇博客有任何错误和建议,欢迎伙伴们留言,你快说句话啊!



概念:

临界资源:指的是多个线程都能访问到的资源;

临界区:能够影响临界资源的代码区域

原子性操作:原子代表该操作仅仅有两个结果要么执行成功要么失败

线程安全:

首先我们要探讨什么样的程序是安全的呢?

最重要的一点,是在任何时候运行时,都能产生我们预期的结果,不会出现错误;

同样,线程安全,我们简单定义就是多个线程运行时,不会导致程序出现二义性结果。

我们可以参照下面的程序,加深对线程安全的理解:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
#define  PTHREAD_NUM 2
int g_val=0;
void* my_thread(void* arg)
{
     (void*)arg;
    for(int i=0;i<100000000;++i)
    {
      g_val++;
    }
    return NULL;
}




int main()
{
   for(int i=0;i<10;++i)
  {
    g_val=0;
    pthread_t tid[PTHREAD_NUM];
    for(int i=0;i<PTHREAD_NUM;++i)
    {
      int ret=pthread_create(&tid[i],NULL,my_thread,(void*)&g_val);
      if(ret!=0)
       { 
        return -1;
       } 
    }
    for(int i=0;i<PTHREAD_NUM;++i)
    {
       int ret=pthread_join(tid[i],NULL);
       if(ret!=0)
       { 
        return -1;
       } 
    }
    printf("%d\n",g_val);   
   }
   return 0;

}

在上面的程序中,我们有一个临界资源 g_val,其值为0 ;之后我们启动两个线程进行++操作,每个线程执行100000000次++操作,两个线程执行完毕之后打印g_val,无论执行多少次,我们期待的结果都是200000000;然而,我们启动改程序,得到的结果并非如此,如图所示,出现了几个并非我们所预料的结果,这证明线程并不是安全的

线程不安全的原理:

刚才我们看到了线程不安全的现象,现在我们来分析一下出现二义性结果的原因是什么?

为了方便我们分析,我们简化一下问题:

现在有两个线程A,B和一个全局变量g_val=0; 每个线程都在入口函数中执行对全局变量g_val执行一次++操作;分析可能导致的结果:

首先我们看一下汇编代码执行++操作需要进行哪些步骤:

1.先将其保存到寄存器当中

2. 将其++

3.保存到内存当中

我们讨论的问题比较简单:无非有两种结果,预期的结果2,和结果1;接下来我们就讨论一下结果1是怎么得来的;

从上面我们已经知道++操作并不是原子性的,完成这个操作需要进行三个动作,而在进行这三个动作的时候,有可能会被操作系统打断,eg:时间片耗尽,进行切换

  • 倘若线程A刚从内存当中取到g_val的值即把0保存到寄存器当中,就切换出去了;线程拥有自己的程序计数器:保存下一个即将执行的指令;上下文信息:保存寄存器当中的值;当再次拥有CPU时,会凭此恢复。
  • 这时候线程B取到了g_val的值即把0保存到了寄存器,CPU进行++运算,将值1写入内存,这时g_val=1;
  • 线程A由重新获取了CPU,进行++运算,得到的值为1,同样将值写回内存,此时仍是g_val=1;
  • 故:我们最后的到的结果可能会有1

可以结合上面画的图在此理解一下上面的过程

线程安全:

要怎么才能保证线程安全呢?

我们需要用到互斥锁、同步变量保证互斥和同步的属性;

互斥:是指在某一时刻仅有一个执行流去访问临界资源

同步:保证执行流访问临界资源的合理性;

对应上面线程不安全的问题,即程序产生二义性,我们可以使用互斥锁来解决;

那么互斥锁是怎样保证原子性的呢?

互斥锁:

互斥锁在底层中是一个互斥量,本质上相当于一个计数器,仅有两个值即0或者1

取值为0的时候,就表示当前互斥锁已经被占用了,无法获取互斥锁,那么也就无法访问临界资源

取值为1的时候,就表示当前互斥锁没有被占用了,可以获取当前互斥锁,然后去访问临界资源

 

获取互斥锁称之为加锁,释放互斥锁称之为死锁;

对于一把互斥锁来说,如果有两个执行流加锁,则只能有一个执行流加锁成功,另一个执行流进行等待加锁(只有得到锁的执行流解锁后,该执行流才能加锁)

对于加锁、解锁来说:

加锁操作在底层中就是将互斥量中的计数器的值进行-1操作

解锁操作在底层中就是将互斥量中的计数器的值进行+1操作

对于互斥锁的加解锁操作而言,其实质也就是对一个变量进行加、减操作,从前面我们知道该操作并非是原子性的,那么互斥锁又是怎么保证线程安全的呢?

互斥锁原理:

对于互斥锁而言,其实加锁和解锁操作都是原子性的

为了保证这两个动作都是原子性的,实现了将寄存器和内存中的值进行互换;xchgb指令,该指令仅有一句,是一个原子操作;

详解:

在加锁的时候,会将寄存器中的值先置为0;

将寄存器中的值与计数器中的值进行互换 ;这个互换动作是原子性的

判断寄存器中的值,为1表示可以加锁,这把互斥锁被其他执行流占用

                                为0表示不可以加锁,得到当前锁资源

在解锁的时候,将计数器中的值置为1

我们可以写一段上面过程对应的伪码进行理解:

lock是指锁中的计数器; movb汇编指令: movb A  B  表示将A值赋给B

加锁过程:

movb  $0 %a1

xchgb %a1  lock

if(寄存器a1的值>0)

     可以加锁,获得锁资源

else

   不可以加锁,锁被占用,执行流阻塞

解锁过程:

movb $1 lock

其实、互斥锁实现的目的就是达到互斥,即防止程序出现二义性、使用时就是用锁锁住临界资源,只有有锁的执行流才能访问临界资源,其余执行流都阻塞在加锁阶段,如下示意:

    加锁

    临界资源

    解锁

而要保证线程安全还有另一个方面,即同步,保证程序出现结果的合理性即正确性

注:学习锁和同步变量的函数,可进入https://blog.csdn.net/weixin_43519514/article/details/105408649

同步:

线程不安全案例:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
#define PTHREADNUM 2
int apple=0;
pthread_mutex_t lock;
pthread_cond_t  cond;
void* Consumer_pthread(void* arg)
{
  (void*)arg;
  while(1)
  {
     if(apple>0)
     {
      pthread_mutex_lock(&lock);
      cout<<pthread_self()<<"我吃第"<<apple<<"个苹果"<<endl;
      sleep(1);
      --apple;
      pthread_mutex_unlock(&lock); 
    }  
  }  
}
void* Producer_pthread(void* arg)
{
  (void*)arg;
  while(1)
  {
     if(apple<=0)
     {
       pthread_mutex_lock(&lock);
       ++apple;
       cout<<pthread_self()<<"新增苹果:"<<apple<<endl;
       sleep(1);
       pthread_mutex_unlock(&lock);
     }
  }
}


int main()
{
  pthread_t consumer[PTHREADNUM];
  pthread_t producer[PTHREADNUM];
  pthread_mutex_init(&lock,NULL);
 
  for(int i=0;i<PTHREADNUM;++i)
  {
      int ret=pthread_create(&consumer[i],NULL,Consumer_pthread,NULL);
      if(ret!=0)
      {
        perror("pthread_create");
        return -1;  
      } 
      ret=pthread_create(&producer[i],NULL,Producer_pthread,NULL);
      if(ret!=0)
      {
        perror("pthread_create");
        return -1;
      }
  
      ret=pthread_detach(consumer[i]);
      if(ret!=0)
      {
        perror("pthread_detach");
        return -1;  
      }
      ret=pthread_detach(producer[i]);
      if(ret!=0)
      {
       perror("pthread_detach");
       return -1;
      }
 }
  while(1);
  pthread_mutex_destroy(&lock);
  
 return 0;
}

上面的代码输出结果: 

从上面的输出结果,我们可以看到,上面的程序存在着不合理性,没有苹果,顾客怎么能消费苹果?所以上面的程序存在着不安全的隐患;

而要解决这个问题,我们就须要用到条件变量,达到同步的效果;

而条件变量的应用于保证多个执行流的合理性

条件变量的原理:

两个接口(等待接口+唤醒接口)+PCB等待队列

条件变量的原理相对简单:

如果当前执行流请求不满足条件,调用等待接口,进入PCB等待队列;

如果其余执行流发现它满足条件的时候,调用唤醒接口,将其唤醒

注:学习锁和同步变量的函数,可进入https://blog.csdn.net/weixin_43519514/article/details/105408649

使用条件变量修正后的程序: 

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
#define PTHREADNUM 2
int apple=0;
pthread_mutex_t lock;
pthread_cond_t  cond;
void* Consumer_pthread(void* arg)
{
  (void*)arg;
  while(1)
  {
     pthread_mutex_lock(&lock);
     while(apple<=0)
     {
      pthread_cond_wait(&cond,&lock);
     }
     cout<<pthread_self()<<"我吃第"<<apple<<"个苹果"<<endl;
     sleep(1);
     --apple;
     pthread_mutex_unlock(&lock); 
     pthread_cond_signal(&cond);

  }  
}
void* Producer_pthread(void* arg)
{
  (void*)arg;
  while(1)
  {

     pthread_mutex_lock(&lock);
     while(apple>0)
     {
        pthread_cond_wait(&cond,&lock);
     }
     ++apple;
     cout<<pthread_self()<<"新增苹果:"<<apple<<endl;
     sleep(1);
     pthread_mutex_unlock(&lock);
     pthread_cond_signal(&cond);
 
  }
}


int main()
{
  pthread_t consumer[PTHREADNUM];
  pthread_t producer[PTHREADNUM];
  pthread_mutex_init(&lock,NULL);
  pthread_cond_init(&cond,NULL);
  for(int i=0;i<PTHREADNUM;++i)
  {
  int ret=pthread_create(&consumer[i],NULL,Consumer_pthread,NULL);
  if(ret!=0)
  {
    perror("pthread_create");
    return -1;  
  } 
   ret=pthread_create(&producer[i],NULL,Producer_pthread,NULL);
  if(ret!=0)
  {
    perror("pthread_create");
    return -1;
  }
  
  ret=pthread_detach(consumer[i]);
  if(ret!=0)
  {
    perror("pthread_detach");
    return -1;  
  }
  ret=pthread_detach(producer[i]);
  if(ret!=0)
   {
    perror("pthread_detach");
    return -1;
  }
 }
while(1);
  pthread_mutex_destroy(&lock);
  pthread_cond_destroy(&cond); 
 return 0;
}

注:如果本篇博客有任何错误和建议,欢迎伙伴们留言,你快说句话啊!

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值