线程的同步与互斥

mutex(互斥量)

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内。这种情况,变量对数单个线程,其他线程无法获得这种变量
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互
  • 多线程并发的操作共享变量,会带来一些问题,因为毕竟不是所有的操作都是原子性的(要么不做,要么全做完,不存在中间时刻)

下面写一个程序来说明:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<string.h>
  5 #include<pthread.h>
  6 
  7 
  8 int ticket = 20;//全局变量,定义20张票
  9 
 10 void *route(void *arg)
 11 {
 12     char *id = (char*)arg;
 13     while(1)
 14     {
 15         if(ticket>0)
 16         {
 17             usleep(1000);
 18             printf("%s sells ticket:%d\n",id,ticket);//打印当前的票和购票人
 19             ticket--;
 20         }
 21         else
 22             break;
 23     }
 24 }
 25 
 26 int main()
 27 {
 28     pthread_t t1,t2,t3,t4;
 29     //创建四个进程,四个购票人
 30     pthread_create(&t1,NULL,route,"thread 1");
 31     pthread_create(&t2,NULL,route,"thread 2");
 32     pthread_create(&t3,NULL,route,"thread 3");
 33     pthread_create(&t4,NULL,route,"thread 4");
 34 
 35     pthread_join(t1,NULL);
 36     pthread_join(t2,NULL);
 37     pthread_join(t3,NULL);
 38     pthread_join(t4,NULL);
 39 
 40 }

运行结果如下:

我们发现出现了票数为负的情况,为什么会这样呢?

  • 四个线程同时访问一个全局变量ticket,都进行了--操作,而--操作并不是原子操作。它会先把ticket变量从内存加载到寄存器中,更新寄存器里面的值,执行--操作,再次寄存器写回到院内从地址中,所以在此期间,很有可能多个进程同时访问该变量,导致结果异常。
  • 操作不是原子的,而是对应下面三条汇编指令
  1. load:将共享变量ticket从内存加载到寄存器中
  2. update:更新寄存器里面的值,执行-1操作
  3. store:将新值,从寄存器写回到共享变量ticket的内存地址

有什么解决办法呢?

  • 解决问题的方法也很简单,就是规定一个线程在申请到该资源用户,其他线程不能再继续申请,只能等待第一个线程释放掉该资源以后,才能进行申请。必须做到以下三点:
  1. 代码必须要有互斥行为:当代码加入临界区执行时,不允许其他线程进入该临界区
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,呢吗只能允许一个线程进入该临界区
  3. 如果线程不在临界区执行,那么该线程不能组织其他线程进入临界区

做到这三点,本质上就是需要一把锁,Linux提供的这把锁叫互斥量

互斥:

概念:事件A与事件B在任何一次事件中不会同时发生,则称事件A和事件B互斥

线程互斥:俩个或多个线程不能同时访问同一块临界资源(共享资源),即多个线程互斥的访问同一块资源

互斥量的接口:

1.初始化互斥量(两种方法)

  • 方法1,静态方法
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  •  方法2,动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr)

参数:

  • mutex:要初始化的互斥量
  • attr:NULL 

2.销毁互斥量

销毁互斥量需要注意:

  • 使用PTHREAD_MUTEX_INITIALIZER(静态)初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destory(pthread_mutex_t *mutex);

互斥量加锁与解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用pthread_lock时,会遇到以下情况

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

改进上面的买票系统:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<string.h>
  5 #include<pthread.h>
  6 
  7 
  8 int ticket = 20;
  9 pthread_mutex_t lock;//创建互斥量
 10 
 11 void *route(void *arg)
 12 {
 13     const void* msg = (const void*)arg;
 14 
 15     while(1)
 16     {
 17         pthread_mutex_lock(&lock);//在访问ticket前上锁
 18         if(ticket>0)
 19         {
 20             usleep(1000);
 21             printf("%s sells ticket:%d\n",msg,ticket);
 22             ticket--;
 23             pthread_mutex_unlock(&lock);//访问结束,释放互斥锁
 24         }
 25         else
 26         {
 27             pthread_mutex_unlock(&lock);
 28             break;
 29         }
 30     }
 31 }
 32 
 33 int main()
 34 {
 35     pthread_t t1,t2,t3,t4;
 36 
 37     pthread_mutex_init(&lock,NULL);
 38 
 39     pthread_create(&t1,NULL,route,"thread 1");
 40     pthread_create(&t2,NULL,route,"thread 2");
 41     pthread_create(&t3,NULL,route,"thread 3");
 42     pthread_create(&t4,NULL,route,"thread 4");
 43 
 44     pthread_join(t1,NULL);
 45     pthread_join(t2,NULL);
 46     pthread_join(t3,NULL);
 47     pthread_join(t4,NULL);
 48 
 49     pthread_mutex_destroy(&lock);//销毁互斥锁,一定要等待线程之后再销毁              
        //因为其四个线程在执行购票操作时,主线程在阻塞式等待,如果主线程先把锁销毁了,
        //就会导致其余线程阻塞,因为都在等待申请锁,或者一个线程在未释放锁时,锁已经被销毁了
 50     return 0;
 51 }


允许结果如下:

加入互斥锁以后,我们发现票数就不会出现负的情况,但是我们同时发现,所有的票都被线程4买走了,这时我们就要引入同步的概念

同步:

  • 概念:指对在一个系统中发生的事件之间进行协调,在事件上出现一致性与统一化的现象
  • 线程同步: 俩个或俩个以上的线程协同的访问共享资源,即线程A访问完之后,它不急着再去申请该资源,而是让B去访问
  • 为了实现多线程之间的同步行为,Linux引入了条件变量,用法和互斥相似

条件变量:

  • 当一个线程互斥的访问某个变量时,他可能发现再其他线程改变状态之前,他什么也不做了
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只有其他线程将一个节点添加到队列中。这种情况就需要用到条件变量

条件变量函数:

1.初始化:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *ret rict attr);
参数:
cond:要初始化的条件变量
attr:NULL

2.销毁:

int pthread_cond_destory(pthread_cond_t *cond);

3.等待条件满足:

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

注意:

  • 要进入临界区对临界资源进行操作,所以首先要申请互斥量
  • 如果发现等待条件满足,则使用wait使线程挂起等待,如果等待条件不满足,就对临界资源进行操作,释放锁
  • 当对临界资源操作完成后,释放互斥量,退出临界区

在执行等待操作时,其实完成了以下事情:

  • 当等待条件满足时,挂起调用他的线程
  • 释放互斥量
  • 当再一次被唤醒并且切换到该线程后,会重新自动获得互斥量,并在等待出继续往下执行 

4.唤醒等待:

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

 当满足条件之后,就要唤醒在条件变量下等待的线程

该函数在使用时,要注意:

  • 进入临界区对临界区资源进行操作,所以要先申请互斥量
  • 使等待条件为假,如插入线程2向队列中插入节点
  • 调用signal,如果此时有线程在等待,则唤醒他,若没有等待的线程,则该函数什么也不做
  • 解锁互斥量,退出临界区

代码如下:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<string.h>
  5 #include<pthread.h>
  6 
  7 pthread_mutex_t lock;
  8 pthread_cond_t cond;
  9 
 10 void *route1(void *arg)
 11 {
 12     while(1)
 13     {
 14         printf("Hello world\n");
 15         pthread_cond_wait(&cond,&lock);
 16         printf("I am thread %d\n",(int)arg);
 17     }
 18 }
 19 
 20 void *route2(void* arg)
 21 {
 22     while(1)
 23     {
 24         pthread_cond_signal(&cond);
 25         printf("I am thread %d\n",(int)arg);
 26         sleep(1);
 27     }
 28 }
 29 
 30 int main()
 31 {
 32     pthread_t t1,t2;
 33 
 34     pthread_mutex_init(&lock,NULL);
 35     pthread_cond_init(&cond,NULL);
 36 
 37     pthread_create(&t1,NULL,route1,(void*)1);
 38     pthread_create(&t2,NULL,route2,(void*)2);
 39 
 40     pthread_join(t1,NULL);
 41     pthread_join(t2,NULL);
 42 
 43     pthread_mutex_destroy(&lock);
 44     pthread_cond_destroy(&cond);
 45     return 0;
 46 }

运行结果如下:

 俩个线程协同工作,互不影响

为什么pthread_cond_wait需要互斥量?

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

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值