欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程的同步与互斥

程序员文章站 2022-05-22 11:21:15
...

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需要互斥量?

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