一、什么叫做线程的同步与互斥?为什么需要同步与互斥?
1、同步与互斥
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。
2、线程的同步与异步
线程同步是多个线程同时访问同一资源,等待资源访问结束,浪费时间,效率低 。
线程异步:访问资源时在空闲等待时同时访问其他资源,实现多线程机制 。异步处理就是,你现在问我问题,我可以不回答你,等我用时间了再处理你这个问题。
同步信息被立即处理 – 直到信息处理完成才返回消息句柄;
异步信息收到后将在后台处理一段时间 – 而早在信息处理结束前就返回消息句柄。
3、多线程的同步与互斥的区别
假如把整条道路看成是一个【进程】的话,那么马路中间白色虚线分隔开来的各个车道就是进程中的各个【线程】了。
①这些线程(车道)共享了进程(道路)的公共资源(土地资源)。
②这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。
③这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。
④这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。
⑤这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道。
注:由于用于互斥的信号量sem与所有的并发进程有关,所以称之为公有信号量。公有信号量的值反映了公有资源的数量。只要把临界区置于P(sem)和V(sem)之间,即可实现进程间的互斥。就象火车中的每节车厢只有一个卫生间,该车厢的所有旅客共享这个公有资源:卫生间,所以旅客间必须互斥进入卫生间,只要把卫生间放在P(sem)和V(sem)之间,就可以到达互斥的效果。
二、mutex(互斥量)
1、多线程的冲突举例
我们知道,多个线程同时访问共享数据时,可能会产生冲突。比如两个线程都要把某个全局变量增加1,这个操作在某平台需要三条指令完成:
1> 从内存读变量值到寄存器
2>寄存器的值加1
3>将寄存器的值写回内存
假设两个线程在多处理器平台上同时执行这三条指令,则可能导致下图所示的结果,最后变量只加了一次而非两次。
数据二义性产生原因:不同的线程访问同一份临界资源,其次线程的操作不是原子操作。
为了验证多线程同时访问临界资源产生的冲突与数据的二义性,创建两个线程,各自把global增加5000次,正常情况下最后global应该等于10000,但事实上每次运行该程序的结果都不一样,代码如下(为了使现象更容易观察到,我们把上述三条指令做的事情用更多条指令来做):
1 /**************************************
2 *文件说明:mutex.c
3 *作者:段晓雪
4 *创建时间:2017年05月29日 星期一 16时11分42秒
5 *开发环境:Kali Linux/g++ v6.3.0
6 ****************************************/
7 #include<stdio.h>
8 #include<stdlib.h>
9 #include<pthread.h>
10
11 int global = 0;//定义全局变量
13 {
14 int i = 5000;//数据太小,线程发生冲突的概率比较小
15 //int i = 5000000;//1、增大循环次数,发生冲突较多
16 //while(i--)
17 //{
18 // global++;
19 //}
20 //2、增多线程切换:内核态---->用户态,增大冲突概率
21 while(i--)
22 {
23 int tmp = global;
24 printf("%d\n",global);
25 global = tmp + 1;
26 }
27 //printf("\n");
28 return (void*)0;
29 }
30 int main()
31 {
32 pthread_t tid1;
33 pthread_t tid2;
34 pthread_create(&tid1,NULL,pthread_add,NULL);//创建线程1
35 pthread_create(&tid2,NULL,pthread_add,NULL);//创建线程2
36 pthread_join(tid1,NULL);//等待线程1
37 pthread_join(tid2,NULL);//等待线程2
38 printf("the global is %d\n",global);
39 return 0;
40 }
第一次运行结果—-5101:
第二次运行结果—-5091:
第三次运行结果—-5000:
3次运行结果均不一样,足以说明多线程在同步访问全局变量global时发生了冲突。
2、解决多线程的冲突问题 —-引入互斥锁
(1)互斥锁:(Mutex,Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得 锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
(2)互斥锁的创建和销毁:
Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁。
1>定义一把锁
pthread_mutex_t lock;
2>互斥锁lock的初始化
如果lock是局部变量—可用初始化函数pthread_mutex_init初始化。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
返回值:成功返回0,失败返回错误号。
如果lock是静态或全局变量—可用 PTHREAD_MUTEX_INITIALIZER初始化 ,相当于用pthread_mutex_init初始化并且attr参数为NULL。
3>加锁与解锁
int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞式加锁
int pthread_mutex_lock(pthread_mutex_t* mutex)//阻塞式加锁
int pthread_mutex_unlock(pthread_mutex_t* mutex)//解锁
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被 另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
Mutex的两个基本操作lock和unlock是如何实现的呢?
假设Mutex变量 的值为1表示互斥锁空闲,这时某个进程调用lock可以获得锁,而Mutex的值为0表示互斥锁已经被某个线程获得,其它线程再调用lock只能挂起等待。
4>销毁锁资源
int pthread_mutex_destroy(pthread_mutex_t* mutex);
返回值:成功返回0,失败返回错误号。
(3)解决多线程的冲突问题
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
int