【加锁 】

锁 理论部分

在这里插入图片描述

定义锁的两种方案
1.定义全局锁
直接在全局用 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
就不用再 init 和 destroy 了。
2.定义局部锁
pthread_mutex_init
pthread_mutex_t 是库提供的一种数据类型
第二个形参是锁的属性,我们不关心直接Null

我们要把对应的资源保护起来,要保证多个线程申请的是同一把锁

共享变量加上锁保护后就叫临界资源
线程中不是每行代码都在访问临界资源
而是这一小块代码在访问临界资源tickets,我们把这一小块临界资源的代码叫做临界区
在这里插入图片描述
加锁是不是一件好事呢?
根据对互斥的学习,任何时刻只允许一个线程去访问临界资源
加锁的本质其实是对被加锁的代码区域让多线程串行访问

加锁不是一个特别好的事情,
加锁的本质: 是用时间来换安全
加锁的表现: 线程对于临界区代码串行执行
加锁原则: 尽量的要保证临界区代码,越少越好!

在循环外面加锁行不行?
在这里插入图片描述

1.从逻辑上不对,因为你难道想让一个线程在while循环中,一个线程把票全抢完然后才能到下一个线程抢吗,下一个线程抢都没票了。
2.而且临界区限定的区域让代码越少越好,临界区代码越少意味着在临界区执行时间越短,越短串行的比率就会降低,更重要的是在临界区里线程也可以被调度的话,临界区代码越短线程被调度的概率也就越低,进而让其他线程等的时间也就变短了

所以这样写是不对的


抢票时每个人先申请锁,谁拿到了锁谁才能进入临界区访问抢票
没有拿到锁的线程,就必须要在指定的锁上等待
申请锁成功,才能往后执行,不成功,阻塞等待。
表现形式就是在pthread_mutex_lock函数这里就卡住了
线程申请锁失败了,线程阻塞等待的本质是线程等待锁资源不就绪,调度器把线程PCB阻塞后去锁的队列里进行等待
在这里插入图片描述
但是这里还有问题,可能一个线程抢到锁了,在抢票时发现票没了直接break
此时解锁代码没有被执行,这个线程已经走了,但是锁一直没释放,其他线程会一直阻塞


正确写法
在这里插入图片描述

验证一下发现所有的票几乎被一个线程都抢走了,一会说这个问题。
但确实解决了抢到负数的问题
在这里插入图片描述
但是这样也不明显,我们再加上一句usleep(13)
在这里插入图片描述

在这里插入图片描述
这里面有非常多细节
刚刚没有usleep,发现一个锁一直被一个线程抢,正常吗?
在这里插入图片描述
正常,每一个线程抢票时,它一释放锁立马循环回去申请锁
线程对于锁的竞争能力可能会不同
因为你离锁最近,别的线程还没来及唤醒呢,唤醒其他线程的成本比持有锁的线程直接再去抢高,他一释放就去抢其他线程可能还要跑很多代码才能抢到

那为什么加了usleep就可以了呢?
在这里插入图片描述
因为一加usleep 这个锁你释放了先别着急申请锁你先等一等
你usleep期间其他线程就有机会拿到锁然后申请了
说明对于锁的访问一定是并发的,只不过因为一个线程竞争能力太强导致其他人抢不到

这份代码其实不太符合逻辑,我们抢到了票,我们会立马抢下一张吗?
不是
因为多线程抢到之后还有后续的工作比如把票信息插入你的名下
所以就用usleep来模拟后续的代码

纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿问题!
但不是说只要有互斥,必有饥饿。适合纯互斥的场景,就用互斥

如果把抢票逻辑写完全了,纯互斥就够了

那该怎么解决饥饿呢?
在这里插入图片描述

1.外面来的,必须排队
假如外面有100个线程,如果持有锁线程释放了,如果外面线程不排队那么OS要唤醒
100个线程全去抢锁,但只有一把锁,也就是说99个都是无效唤醒,这不合理
所以要排队

2.出来的人,不能立马重新申请锁,必须排到队列的尾部

让所有的线程(人)获取锁(钥匙),按照一定的顺序。按照一定的顺序性获取资源—同步!!
不一定非得按照队列,爱是什么结构是什么,只要有顺序性


锁本身就是共享资源 ! !
所以,申请锁和释放锁本身就被设计成为了原子性操作(如何做到的?)
不用担心申请锁多线程并发申请的问题,有且只有一个线程能得到锁


在临界区中,线程可以被切换吗???
肯定可以被切换

tickets- - 三行代码都可以切换,这临界区那一大坨代码,线程在任何地方都可以去切换
那切换了锁怎么办会不会出问题呢?
当线程在临界区中在任何地方都会被切换,但是当前线程不怕被切换,因为线程已经申请了锁并且并没有解锁,所以在线程被切出去的时候,是持有锁被切走的。
即便我不在期间,我只要没执行解锁,任何线程都进不去临界区访问临界资源。

===============================================================

我们今天为什么要加锁呢?
因为我们有并发问题
那为什么有并发问题呢?
因为我们使用多线程访问了全局变量
为什么要有多线程呢?
因为我们想提高代码的并发度而且不想创建进程那么重而是通过线程的方式简单创建

把线逆向推回去

为了提高并发度且要降低成本使用多线程
使用多线程就有线程间资源共享,虽然解决了并发度问题,但引入了多线程访问数据不一致问题
为了解决数据不一致所以引入了互斥锁,互斥锁使用时就要考虑临界资源,临界区,原子性等
单纯互斥锁还可能引发饥饿问题。

任何解决方案伴随着新的问题,所以这个世界上没有放之四海而皆准的法则,没有一套固有的一套方式能够把任何问题全部解决的。看看这个问题会不会影响我们最看重的,如果不关心那就不管了。

锁的原理

原子性的概念就是一件事情要么做了,要么没做,它没有中间状态
原子性可能存在很多场景

我们认为一条汇编语句已经是计算机里执行的最基本的指令了
今天得给原子这个概念下个定义,我们认为一条汇编语句就是原子的!
调度器调度时,一条代码是不会被CPU中断的,要么执行完了,要么不执行

下面谈一谈互斥锁是如何实现的

为了实现互斥锁操作,大多数体系结构都提供了swap or exchange指令
(大部分体系结构就是大部分CPU架构,CPU中有自己的指令集,就是内置了最基本的指令)
该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性
结论
体系结构提供了一条汇编指令exchange 把寄存器里的值和内存里的值做交换
因为是一条汇编,所以是原子的。

下面来看一段加锁和解锁的伪代码
在这里插入图片描述

lock 对应的就是 pthread_mutex_lock();

对于锁呢不用理解的太复杂,我们看来锁就是一个简单的变量,
定义一把锁,定义成局部或全局的其实就是定义对象
我们目前把锁当成一个内存里的整数 int mutexi = 1 表示锁存在

CPU里有eax 和 al寄存器 他们俩当成一个就行了
在这里插入图片描述

首先一个线程进来要执行伪代码第一句,把mov 把al寄存器清零
第二件事,把al寄存器里的内容和mutex变量里的内容交换
这个动作其实就是申请锁的动作
做完交换后他就拿到锁了,接下来就判断寄存器al里的值是不是大于0
如果是,return 0 代表申请锁成功,
如果不大于0,也就是申请锁失败 当前线程要进行等待。

上面是正常情况,下面说说特殊情况

当前是一个线程在执行代码哦
我的问题是,有没有可能线程1 刚把寄存器al置零了,这个线程就被切换了
线程2 也来申请锁了
在代码的任何地方被切换都是有可能的,别告诉我你现在在加锁
加锁函数实现里这么多汇编语句,上次讲tickets - - 时你都说任何地方都能切换,所以刚执行完置零线程被切换是有可能的

CPU的寄存器只有一套,但CPU的寄存器里的内容是每一个线程都要有一个自己的内容的
寄存器 != 寄存器内容
线程1要被切换走的时候必须把自己的上下文带走,包括al寄存器里的0
并且还要记录下来当前执行到哪里了方便继续往后执行xchgb交换
所以线程1 就走了
线程2 来了,假设线程2 非常顺利不会被别人所中断
它也要把寄存器al写0,实际上是写到了线程2 的硬件上下文中
线程2 要xchgb把寄存器al的值和内存mutex交换 al是1
线程2 继续要判断寄存的内容,正准备判断时线程2 被切换走了
它走的时候要把寄存器里内容全部带走,也就是把al 是 1 带走,并且记录他走到 i f 位置
在这里插入图片描述

所以线程2 就被切换走了
然后把线程1 拿回来,它回来的时候 首先要恢复曾经带走的上下文,也就是al寄存器被恢复为0
然后继续把寄存器al的值和内存mutex交换,交换后线程1的al还是0,判断后直接走else挂机等待
线程1申请锁失败,线程1不会被调度了因为他被阻塞了,
所以线程2回来了,它也要先恢复自己的上下文,然后执行if 判断,发现al 是1 大于0,return申请锁成功
在这里插入图片描述

伪代码最核心的其实是xchgb交换
交换的本质是什么呢?
把内存中的数据(就是这把锁本质被所有线程共享),(线程执行)交换到CPU的寄存器中
本质是把数据交换到线程的硬件上下文中!!
线程的上下文是线程私有的!
所以把内存中的数据交换到寄存器里,本质是把一个共享的锁,让一个线程以一条汇编的方式,交换
到自己的上下文中!
一旦交换到自己的上下文中,所有线程申请锁都是用0和内存交换的,整个交换中1只有1个
交换到自己的上下文中,最终代表当前线程持有锁了!

最开始mutex变量是1,所有人都和我换,看起来是换到寄存器硬件了,但其实是换到了当前执行线程的上下文中,一旦换到它里面它线程一旦被切走它自己的上下文就保护了,其他线程也看不到 , mutex变量也是0 ,其他人也申请不到了

所以1就在内存和线程的上下文之间,1就跟令牌一样在多个线程内自由流动
所以竞争锁本质就是看谁运气好,更快的把exchange指令做完

至此就完成了加锁

再谈谈解锁
解锁的时候原子性重要吗?

在这里插入图片描述
mov就一条汇编 把mutex变量置1 是原子的

我们想的应该是线程里上下文的锁再交换回内存中Mutex变量,意味着加锁解锁必须是同一个线程
这里的unlock可以是其他线程来解锁

在编码上来说可以让其中某一个线程不加锁直接访问临界资源,其他线程加锁遵守互斥规则
但是这是有问题的,一旦有了互斥规则所有线程都应该遵守

锁的应用 — 封装

#pragma once

#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t* lock)
    :lock_(lock)
    {}
    void lock()
    {
        pthread_mutex_lock(lock_);
    }
    void unlock()
    {
        pthread_mutex_unlock(lock_);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t* lock_;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* lock)
    :mutex_(lock)
    {
        mutex_.lock();
    }
    ~LockGuard()
    {
        mutex_.unlock();
    }
private:
    Mutex mutex_;
};

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值