线程的同步和互斥
1. 线程互斥
与互斥相关的背景概念
- 临界资源:多个执行流共享的资源叫做临界资源。
- 临界区:每个线程的内部,访问临界资源的代码,叫做临界区。
- 原子性(两态):不会被任何调度机制打断的操作,意思就是要操作就操作完它,要么就压根不去执行它。
互斥的概念
- 在任何时刻,保证只有一个执行流进入临界区访问临界资源,通常对临界资源起保护作用。
互斥量的引入-----(锁)
下来我们先来看一段代码:
用多线程来模拟抢票系统
#include< stdio.h>
#include< stdlib.h>
#include< unistd.h>
#include< pthread.h>
int ticket=10; //临界资源
void* get_ticket(void *arg)
{
int* num=(int*)arg;
while(1){
if(ticket>0){
usleep(100); //等待(让多个线程进入)
printf("thread%d ,get a ticket,residue:%d\n",*num,ticket-1);
ticket--;
}
else{
break;
}
}
}
int main()
{
pthread_t tid[4];
int arr[]={0,1,2,3};
int i=0;
for(;i<4;++i) // 用循环创建线程
{
pthread_create(tid+i,NULL,get_ticket,(void*)(arr+i));
}
i=0;
for(;i<4;++i)
{
pthread_join(tid[i],NULL);
}
return 0;
}
我们来看实验结果:
为什么无法获得与现实相符的结论呢?
- if语句判断成功后,代码可以并发的切换到其它线程
- uslee 这个模拟漫长的业务过程中,可能会有多个线程进入该代码段。
- ticket-- 这个操作本身不是一个原子操作。
要解决上面的问题,就需要做到:
- 代码必须要有互斥行为:当有一个线程进入临界区时,不允许其他线程进入。
- 如果有多个线程同时要求执行临界区代码,并且没有代码在临界区执行时,那么只允许一个线程进入。
- 如果线程不在临界区执行时,那么该线程不能阻止其他的线程进入临界区。
终于“千呼万唤始出来” 要做到以上三点,本质上就是需要一把锁。
Linux上提供的这把锁叫互斥量。
锁的使用方法
互斥量的使用(接口)
初始化互斥量
两种方法:
- 法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
销毁互斥量
int pthread_mutex_destroy(pthread_mutex *mutex);
销毁互斥量要注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值: 成功返回0, 失败返回错误号
调用pthred_mutex_lock 时,还会遇到的情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
改进上面的售票系统:
#include< stdio.h>
#include< stdlib.h>
#include< unistd.h>
#include< pthread.h>
int ticket=10;
pthread_mutex_t lock; //定义一个锁
void* get_ticket(void *arg)
{
int* num=(int*)arg;
while(1){
usleep(100);
pthread_mutex_lock(&lock); //访问临界资源前加锁
if(ticket>0){
usleep(1000);
printf("thread%d ,get a ticket,residue:%d\n",*num,ticket-1);
ticket--;
pthread_mutex_unlock(&lock); //访问完毕解锁
}
else{
pthread_mutex_unlock(&lock);
break;
}
}
}
int main()
{
pthread_t tid[4];
int arr[]={0,1,2,3},i=0;;
pthread_mutex_init(&lock,NULL); // 初始化该锁
for(;i<4;++i)
{
pthread_create(tid+i,NULL,get_ticket,(void*)(arr+i));
}
i=0;
for(;i<4;++i)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&lock); //销毁
return 0;
}
结果展示:
锁的实现原理 (是如何保证原子性的)
- 为了实现互斥锁的操作,大多数的体系结构都提供了swap或exchange指令,该指令的作用是吧寄存器和内存单元的数据相交换,由于指令只有一条,所以保证了原子性。
下来我们具体来理解一下:
- 看完图我们来总结一下:
- 锁的底层(是如何保证原子性的)
-
- 保证 ”1“ 只有一份。
-
- exchange 一条汇编完成了寄存器与内存数据的交换。
2.线程同步
什么是同步
- 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源。
为什么需要同步
- 为了让多线程协同高效的完成某些事情
怎样做到同步(条件变量)
什么是条件变量
条件变量是利用线程之间共享的的全局变量进行同步的一种机制。
条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的。
有关使用条件变量的函数
初始化:
int pthread_cond_init (pthread_cond_t *restrict cond,const pthread_condatter_t *restrict attr);
- 参数:cond:要初始化的条件变量; attr :NULL
销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足:
int pthread_cond_wait (pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
- 参数:cond:要在这个条件变量上等待;mutex: 互斥量
唤醒等待:
-
int pthread_cond_broadcast(pthread_cond_t *cond); //只要是在这个条件变量下等的线程都被唤醒
-
int pthread_cond_signal(pthread_cond_t *cond);//只唤醒一个在该条件变量下等的线程
简单运用:让两个线程交替打印
#include< stdio.h>
#include< unistd.h>
#include< pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void* routine_1(void*arg)
{
char*name=(char*)arg;
while(1){
pthread_cond_wait(&cond,&mutex);
printf("%s:i am processA\n",name);
}
}
void *routine_2 (void*arg)
{
char*name=(char*)arg;
while(1){
pthread_cond_signal(&cond);
sleep(1);
printf("%s:i am processB\n",name);
}
}
int main()
{
pthread_t t1,t2;
pthread_cond_init(&cond,NULL);
pthread_mutex_init(&mutex,NULL);
pthread_create(&t1,NULL,routine_1,"processA");
pthread_create(&t2,NULL,routine_2,"processB");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
结果展示: