TTTTTTZZZZZZ(系统编程--线程) 12

线程不安全:多线程环境下程序的执行结果出现预料之外的值。
举一个例子:假如现在有一个全局变量,两个线程分别执行,对这个数据进行++50000次,执行完之后我们发现输出的结果并不是我们想要的100000,而是一些每次都在改变的值,这就属于线程不安全。

多个线程访问的那个公共的资源就叫做“临界资源”--------对应上面例子中的全局变量
访问临界资源的代码就做“临界区”
在临界区中使用“互斥机制”就能够解决线程不安全的问题

互斥机制的使用:

1.先加🔒
2.执行临界区代码
3.释放🔒 同一时刻只能有一个线程获取到🔒,只有这个获取到🔒的线程才能执行临界区的代码,其他线程只能等待🔒的释放。

互斥锁/互斥量:
互斥锁的主要作用是将并行的程序改成了串行了
加锁需要五个步骤
1.先定义一个🔒

pthread_mutex_t mutex;

2.初始化🔒

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

pthread_mutex_init(&mutex,NULL);

3.在需要加🔒的位置上🔒

int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_mutex_lock(&mutex);

4.代码执行完毕解🔒

int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_unlock(&mutex);

5.销毁🔒

int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_destroy(&mutex);

如果当前锁已经被其他线程获取了,当前线程在想获取,就会阻塞到lock()加锁函数这里,直到锁资源被释放,才会继续执行。这个是便于理解的,实际上本质是,下面这段话。

互斥锁pthread_mutex又叫挂起等待锁,一旦线程获取锁失败,就会挂起(进入操作系统的一个等待队列中,这个线程什么时候才能恢复执行,也不是其他线程释放锁,立刻就能执行起来,他还要等待cpu的调度才能执行,调度也是有开销的)

互斥锁能够保证线程安全,但是最后的程序效率会受到影响
除此之外还有可能引起死锁,锁上之后却忘记解锁,就会造成死锁。

死锁的常见场景:

1.一个线程加锁之后,再次加锁就会造成死锁
2.两个线程有两把锁,锁a,锁b,线程1先获取锁a,再去获取锁b,线程2先获取锁b,再去获取锁a,此时两个线程就会进入死锁状态。双方都在锁在了,尝试获取对方线程手中的锁的这个状态。死锁
3.哲学家就餐问题,多个线程,多把锁问题。

实际规避死锁的方法是:
1.短:让临界区的代码尽可能的去短,一目了然,这样就算死锁也会很容易看出来
2.平:在临界区中尽量不要有复杂的函数调用
3.快:临界区代码执行速度尽量快,别做太耗时的操作

真正出现死锁了,那也只有想办法解决了,代价也是比较大的

哲学家就餐问题解决示意图:

在这里插入图片描述
可重入函数:一个函数在任意的执行流中调用,都不会出问题

线程安全函数:一个函数在任意的线程中调用,都不会出问题

可重入这个概念要求更高一些,涵盖了线程安全。如果一个函数是可重入的,一定线程安全,反之,如果一个函数线程安全,不一定可重入,可重入的概念在之前已经说过了。

//为了验证上面的理论,看下面这段代码
  1 #include<stdio.h>                                                                                                                 
  2 #include<pthread.h>
  3 #include<unistd.h>
  4 #include<signal.h>
  5 pthread_mutex_t mutex;
  6 int g_count = 0;
  7 void ModifyCount()    //这个是线程函数内部的函数,加锁了,所以线程安全
  8 {
  9   pthread_mutex_lock(&mutex);
 10   ++g_count;
 11   printf("before sleep\n");
 12   sleep(3);
 13   printf("after sleep\n");
 14   pthread_mutex_unlock(&mutex);
 15 }
 16 void* func(void* arg)   //线程所执行的函数
 17 {
 18   (void)arg;
 19   for(int i = 0;i < 50000;++i)
 20   {
 21     ModifyCount();  //线程执行的内部函数
 22   }
 23   return NULL;
 24 }
 25 
 26 void MyHandler(int sig)
 27 {
 28   (void)sig;
 29   ModifyCount();  //线程和信号捕捉函数执行的是相同的代码
 30 }
 31 int main ()
 32 {
 33   signal(SIGINT,MyHandler);  //信号捕捉函数,捕捉二号线好
 34   pthread_mutex_init(&mutex,NULL);
 35   pthread_t tid[2];
 36   for(int i = 0;i < 2; ++i)
 37   {
 38     pthread_create(&tid[i],NULL,func,NULL);
 39   }
 40  for(int i = 0;i < 2 ;++i)
 41  {
 42    pthread_join(tid[i],NULL);
 43  }
 44  printf("g_count = %d\n",g_count);  
 45  pthread_mutex_destroy(&mutex);
 46  return 0;
 47 }                                

在这里插入图片描述

代码运行起来之后就是上图的样子,信号捕捉函数,捕捉ctrl + c 2号信号,捕捉到之后,执行线程函数内部的加法函数,线程函数加锁 了所以是安全的,但是在线程函数加了锁之后,大印before之后我们在,连续输入2号信号,此时我们要清楚信号捕捉函数的执行流程,捕捉到之后,由内核执行信号处理函数,和我们的进程属于不同的执行流,在信号处理函数执行的时候,进程代码全部停止,等待信号处理函数处理完毕之后再继续执行,但是此时外部的线程已经打印before ,还没有打印after 就说明线程还没有释放锁资源,此时信号处理函数再加锁,这样就陷入了死锁。

综上所述,所以说可重入要求更高一些,线程安全了不一定可重入,可重入了一定线程安全。

可重入的概念

信号处理函数很少用到,因为一旦进入信号处理函数,原来的逻辑不管有多少个线程都得停下来等,除非极个别的情况下会使用

同步与互斥:同步是同步,互斥是互斥,不要混为一谈
互斥:就是同一时间只有一个线程可执行临界区。

同步:同步控制着线程和线程之间的执行顺序(主要还是抢占式执行惹的祸,有的时候我们就是需要线程和线程之间按照一定的顺序来执行)。

举个例子:一个人在ATM机取钱,后面有一大堆人在排队,第一个取钱的人,锁上门,取钱,完事之后解锁,出来,但是他刚出来,就想到钱没拿够再拿一点这样的话,后面排队的人就没有办法取钱了。
线程饿死

然后再说线程 ,之前说了当一个线程获取锁之后,先一个线程假如也在等这个锁(死等),等到锁被释放了要等到cpu继续调度这个线程才可以获得锁,但是线程与线程之间是抢占式的执行,大家都想抢到只有一份的资源这样,加入第一个获取锁的线程反复自己一个线程利用资源,因为自己加完锁,释放后,在获取锁比较方便,但是后面的线程没法获取到锁资源了,就在一直等,这就叫线程饿死。

抢占式执行是万恶之源

所以就需要同步,来控制线程的执行顺序,这时候就需要引入条件变量。

同步代码:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<pthread.h>
  4 #include<stdlib.h>                                                                                                                
  5 pthread_mutex_t mutex;
  6 pthread_cond_t cond;         
  7 void* func1(void* arg)       
  8 {                            
  9   (void)arg;                 
 10   while(1)                   
 11   {                          
 12     pthread_cond_signal(&cond);   //要被等的执行这个函数
 13     printf("传球\n");        
 14     usleep(789789);          //一个时间长
 15   }                          
 16   return NULL;               
 17 }                            
 18 void* func2(void* arg)       
 19 {                            
 20   (void)arg;                 
 21   while(1)                   
 22   {                          
 23     pthread_cond_wait(&cond,&mutex);   //需要等其他线程的执行这个函数,阻塞在这里直到条件成立
 24     printf("扣篮\n");        
 25     usleep(123123);         //一个时间短 
 26   }                          
 27   return NULL; 
 28 }
 29 int main()
 30 {
 31   pthread_t tid[2];
 32   pthread_mutex_init(&mutex,NULL);  //初始化锁
 33   pthread_cond_init(&cond,NULL);  //初始化条件变量
 34   pthread_create(&tid[0],NULL,func1,NULL);  //线程1
 35   pthread_create(&tid[1],NULL,func2,NULL);  //线程2
 36     
 37   pthread_join(tid[0],NULL);  //线程等待
 38   pthread_join(tid[1],NULL);
 39   pthread_mutex_destroy(&mutex);  //释放锁
 40   pthread_cond_destroy(&cond); //释放条件变量
 41   
 42   
 43   return 0;
 44 }  
//执行结果:
传球
扣篮
传球
扣篮
传球
扣篮
传球
扣篮
传球
扣篮
传球
扣篮
            整齐          

pthread_cond_wait做了以下几件事情:

1.先释放锁
2.等待条件就绪
3.重新获取锁,准备执行后续的操作 大部分情况下,条件变量都得和互斥锁一起使用。

又有一个问题就是很有可能等待的线程,在条件变量已经满足之后,却没有接收到通知,所以,要求1,2两步必须是原子性的,也就是说两步是不可以拆分的 ,为的就是可以第一时间接收到通知。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值