为了保护这些共享资源在被使用的时候,不会受到其他线程的影响,因此我们要为临界区加锁。c++11已经支持了互斥锁mutex,mutex本质上还是封装了pthread库中原生的互斥锁,下面要介绍的是pthread库提供的原生互斥锁,pthread库的互斥锁更底层,更接近系统级调用。
目录
4、上锁 pthread_mutex_lock、解锁 pthread_mutex_unlock
一、互斥锁相关函数
pthread给我们提供了“互斥锁”这种数据类型 —— pthread_mutex_t,除此之外还有初始化锁、加锁、解锁、销毁锁等相关的函数。
1、创建锁
(1) 方式一:仅仅只是创建锁
这步其实就是在声明一个锁的变量
pthread_mutex_t mtx; //创建一把锁
(2) 方式二:创建锁+初始化锁一步到位
一般在main函数中创建然后把锁的地址传递给子线程,或者直接在最前面加一个static,这样的话就不是在主线程的栈上开辟,而是在静态区开辟,无需传递锁的地址,因为所有的线程都能访问到静态区。
2、初始化锁 pthread_mutex_init
创建出锁以后,需要给这把锁初始化,你可以理解为设置锁的密码。
第一个参数指的是要给哪个锁初始化;第二个参数是设置锁的属性,这个老样子,我们不懂,交由pthread库决定,一般设置为NULL
返回值:成功返回0,失败返回一个错误码
3、销毁锁 pthread_mutex_destroy
现在我们不需要这个锁了,我们要把这个锁销毁,注意!销毁锁 ≠ 解锁
参数就是你要销毁哪把锁;返回值:成功返回0,失败返回一个错误码
4、上锁 pthread_mutex_lock、解锁 pthread_mutex_unlock
这两个函数的参数都一样,都指的是要使用哪把锁;成功返回0,失败返回错误码
这两个函数一般需要搭配使用
pthread_mutex_t mtx;
pthread_mutex_lock(&mtx); //加锁
//...受到锁保护的临界区
pthread_mutex_lock(&mtx); //解锁
二、互斥锁解决资源冲突
现在我们来解决之前的抢票问题
=======================主线程 =======================
=======================子线程 =======================
测试结果如下,此时留意一下线程ID,你会发现最后900张票都是一个线程抢的,这是因为没有了其他线程的干扰,一个线程能完整的走完自己的时间片,一个线程的时间片虽然短,但是能抢一批
三、互斥锁的底层原理
你是否注意到,我们在上述过程中只用了一把锁,要达到互斥的目的,那就要求所有的线程都能看到这把锁,也就是说 互斥锁 也是临界资源,既然所有的线程都能申请锁,那不就乱套了吗?
答案是并不会,并不是所有的线程都能申请成功!
1、锁创建成功并初始化以后
当我们把锁创建出来,并初始化以后,内存中就会有一个变量叫做mutex,此时mutex = 1,先这么理解,谁拿到这个 1,就代表谁拥有锁!此时很显然,内存持有锁
2、加锁和解锁的简单理解
(1) 加锁
======================线程A申请锁======================
假设一个线程A申请锁的时候,实际上站在汇编的角度执行的就是下面的内容
第一步,movb 在汇编中表示赋值,后者赋值给前者,%al 代表CPU里的寄存器,$0 代表0。当线程A申请锁的时候,先把 0 放到寄存器中 (初始化寄存器)。
第二步,xchgb 在汇编中表示交换,前者的值和后者交换,%al表示CPU里的寄存器,mutex表示上面创建的变量。将寄存器中%al的值和内存中mutex的值交换。
第三步,此时是线程A被调度,在A的时间片内,%al寄存器是A私有的,里面的数据也是,这就表明线程A 持有锁,此时寄存器的值大于0,可以访问临界区。这是申请成功的情况。
======================线程B申请锁======================
线程A 的时间片到了,但是A没有解锁,此时线程A会把 %al 里的值 也就是 1 放到自己的PCB里,排到队尾。然后线程B来申请锁了。
第一步,此时线程B被调度,%al是线程B私有的,依然是先把 %al 的值赋值为 0,然后和 内存的mutex交换。但是mutex = 0说明此时锁不在内存手里,%al交换以后依然为0
第二步,寄存器%al的值为0,if语句条件不通过,此时线程B就会被挂起,无法访问临界区。这是申请失败的情况。
(2) 解锁
现在线程A来解锁了,解锁对应的汇编内容如下
此时把mutex的值赋值为1,也就是把锁还给内存中的mutex变量,然后环形等待mutex的线程。比如唤醒线程B,然后线程B就会去执行上面申请锁的汇编语句。
3、互斥锁总结
使用互斥锁的代码可以简化为下面这个模型
如果线程在向下执行的过程中,申请了锁,如果成功,那么你就可以访问临界区;但是!如果线程申请失败了,那么该线程会被挂起,不会跳过加锁的区域,然后向下运行。
如果线程在向下执行的过程中,因为if语句条件不通过,导致没有申请锁这个过程,那么该线程就不会被挂起
==》只有申请了锁,而且申请失败了,那么这个线程就会被挂起,直到上一个线程解锁以后才会被唤醒