@TOC
1. 背景概念
多线程中,存在一个全局变量,是被所有执行流共享的
根据历史经验,线程中大部分资源都会直接或者间接共享 只要存在共享,就可能存在被并发访问的问题
假设有一间教室被学校内的所有社团共享的,所以这个教室属于公共资源,
有可能当一个社团在这个教室举办活动时,别的社团也想占用这个教室
即 一个公共资源被并发访问了 为了保证访问时不能被别人去抢走,所以就把门窗都关上,直到访问完,才让别人进来
即 发生互斥
为了保证对应的共享资源的安全,用某种方式将共享资源保护起来,这部分共享资源称之为临界资源
访问临界资源执行的代码 称之为 临界区
多个线程对全局变量做-- 操作
假设有一个全局变量 g_val=100
有两个 线程A 和 线程B,分别对同一个全局变量g_val进行--操作
第一步g_val变量要修改,要把内存的数据load到寄存器中
第二步在寄存器内部,进行数据的--操作
第三步把在寄存器中修改后的数据写回到内存中
g_val--,在C语言上是一条语句,但实际上至少要有三条语句
线程A执行g_val-- 操作
第1步把数据load到寄存器中,第2步在寄存器中对数据做--操作 线程A正准备做第3步时,时间片到了,线程A不能继续向后运行了
线程A要把自己的上下文保护起来,并且将寄存器中的数据也带走了
线程a认为值已经被改成99了,并且还有第三条语句还没有执行
线程B执行 g_val-- 操作 第1步把数据load到寄存器中,
线程B认为g_val没有被写过,所以g_val依旧从100开始修改
第2步在寄存器中对数据做--操作
第3步把修改后的数据写回内存中,即将内存中g_val从100改成99
假设线程B通过while无线循环,则把g_val修改了90次后,g_val值变为10,
此时再次执行时间片到了,所以无法执行第3步,把线程B的上下文保存起来
此时再次执行线程A,由于上次执行线程A时第3步没有执行,所以线程A继续执行第3步
但是内存中的g_val为上次线程B修改后的值10,又被改为99了 把线程B做的数据修改干掉了
对全局变量做--,没有保护的话,会存在并发访问的问题,进而导致数据不一致 g_val被称为 共享资源, 对共享资源进行一定的保护即 临界资源 用来衡量共享资源的 任何一个线程 都有自己的代码访问临界资源,这部分代码 被称为 临界区
同样存在不访问临界资源的区域 被称为 非临界区
用于 衡量 线程代码的
让多个线程安全的访问临界资源 —— 加锁 即完成互斥访问
把三条指令,看起来就像一条指令 被称为 原子性 (要么就不执行,要执行就都执行)
2. 证明全局变量做修改时,在多线程并发访问会出问题
创建一个全局变量 tickets 作为票数,并创建4个线程,
分别调用自定义函数 thread_run 来对tickets进行--操作 ,直到tickets的值 所有线程都必须要先看到同一把锁 -> 锁本身就是公共资源
->锁如何保证自身安全? ->加锁和解锁本身就是原子的
(原子性:要么就不加锁,要加锁就加成功)
锁的申请是安全的,就可以保证锁保护的资源本身也是安全的
4. 临界区可以是一行代码,也可以是一批代码
访问全局资源时,可能会存在多并发访问的问题
切换会有影响吗?
加锁在临界区内,加锁后,对临界区代码进行任意切换会不会影响数据出现安全方面的问题? 不会,我不在期间,其他人没有办法进入临界区,因为无法成功申请到锁,锁被我拿走了
存在一个VIP自习室,一次只能有一个人
这个自习室有一个特点,无人值班,门旁边有一把钥匙,门默认是锁着的
若小明想要到这个自习室进行自习,就需要拿到钥匙,把门打开 ,才可以使用自习室
当小明进来后,为了防止别人打扰,把门进行反锁,同时钥匙在小明口袋中
其他人是没办法进来 这个门被反锁的自习室
突然在自习室内的小明 想去上厕所,但是他还想继续自习
所以去上厕所之前,把门又从外面锁上了,把钥匙再次装入口袋中 上厕所期间,并不担心有人进入自习室,因为被锁住了
申请锁后,相当于把锁拿到自己手上了,同时其他人就无法申请了
当访问临界区时,有可能被挂起被阻塞,但是并不担心别人进入临界区中 此时并没有解锁,没有归还锁, 即便当前线程不在, 其他线程也无法调度
5. 互斥锁的原理
背景知识
1.为了实现互斥锁,大多数体系结构(CPU)提供了 汇编指令 即 swap或exchange指令
指令作用为 把寄存器和内存单元的数据相交换
将CPU中的数据与 内存中的数据进行交换
按照传统做法,一条汇编做不到,所以需要借助 一个临时空间进行保存,然后才能进行交换
体系结构为了支持锁的实现,提供了 swap /exchange 指令
一条汇编,把 CPU的数据与 内存中的数据做交换
只有一条汇编指令,保证了原子性
2.寄存器的硬件只有一套,但是寄存器内部的数据是每一个线程都要有的
寄存器 != 寄存器内容(执行流的上下文)
具体实现
用互斥锁这样的类型定义变量,在内存里开辟空间
默认mutex等于1
以线程为单位,调用这部分加锁的代码
并不是线程自己去调,而是要让CPU去跑,CPU会去执行线程的代码
CPU上有一个寄存器,其被命名为 %al
假设 有线程a (thread a) 和线程b (thread b),都要执行加锁的任务
执行加锁对应的伪代码的第一个指令, 即先把0放入寄存器中
所以当线程a把数据放入寄存器中,这个数据依旧属于线程a的上下文
第一条指令 本质为 调用线程,向自己的上下文写入0
第二条指令,将cpu的寄存器中的%al 与 内存中的mutex 进行交换 交换的本质是 :将共享数据交换到 自己的私有的上下文中 所有线程看到的是同一把锁,mutex作为共享数据 ,交换到寄存器的上下文中,寄存器作为线程的私有上下文 即 加锁 数据1 就可以被看作是锁
交换 只有 一条汇编指令 ,要么没交换,要不就交换完了 即加锁的原子性
判断al寄存器中的内容是否大于0,
若大于0,返回0,代表加锁成功
假设线程a 即将执行对于判断时 ,进行线程切换,
此时线程a 要带走自己的上下文 即 al寄存器的值为1 ,同时记录下即将执行判断
切换成线程b,继续执行前两条指令 ,先将 al寄存器数据置为0
再将寄存器中的数据 与 内存中的数据 进行 交换
线程b 继续执行时 要进行判断 ,寄存器数据不大于0,当前线程被挂起
线程b申请锁失败
线程b 带走了自己的上下文 即 寄存器中的数据为0
再次切换成 线程a,带回来线程a的寄存器数据 1,并继续执行 上次还未执行到的判断
线程a的寄存器中的数据大于0,返回0,申请锁成功