文章目录
- 1. 背景概念
- 多个线程对全局变量做-- 操作
- 2. 证明全局变量做修改时,在多线程并发访问会出问题
- 3. 锁的使用
- pthread_mutex_init
- pthread_metux_destroy
- pthread_mutex_lock 与 pthread_mutex_unlock
- 具体操作实现
- 设置为全局锁
- 设置为局部锁
- 4. 互斥锁细节问题
- 5. 互斥锁的原理
- 背景知识
- 具体实现
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的值<0才结束
创建一个全局变量 tickets 作为票数,并创建4个线程,
分别调用自定义tickets变为负数 ,是不合理的
在我们设计中,若ticjets<0就会直接break退出,只有当tickets>0时才会打印出对应tickets的值
假设 tickets==1 ,此时有 a b c d 4个线程
当线程a 通过判断 进入 if语句中的 sleep中时 ,被上下文保护了
线程b 也执行判断 进入 if语句,继续向下执行完 tickets-- ,
此时的tickets的值为0,CPU就会再次执行还未执行完的线程a 的剩余步骤,tickets-- 即 0-1 =-1
3. 锁的使用
为了避免全局变量 出现负数的情况,所以引入 加锁 用于保证共享资源的安全
pthread_mutex_init
输入 man pthread_mutex_init
第一个参数 为 互斥锁,对该锁进行初始化,初始化该锁处于工作状态
第二个参数 为属性 一般设置为 nullptr
一般有两种初始化方案
第一种,锁为全局变量 ,直接用PTHREAD_MUTEX_INITIALIZER
,对锁进行初始化
后面就不用 通过pthread_mutex_destroy 对其进行摧毁
第二种,若锁为局部变量,就必须调用pthread_init 进行初始化,用完后也必须调用 pthread_destroy 进行销毁
pthread_metux_destroy
参数为锁
对锁进行销毁
若锁为局部变量
则需要在创建线程之前初始化,使用完线程后在销毁
pthread_mutex_lock 与 pthread_mutex_unlock
输入 man pthread_mutex_lock
加锁
参数为 锁
对该锁进行加锁
若加锁成功就会进入临界区中访问临界区代码
若加锁失败,就会把当前执行流阻塞
输入 man pthread_mutex_unlock
解锁
对该锁进行解锁
具体操作实现
设置为全局锁
若锁为全局变量,可以选择在主函数中初始化锁 与销毁锁
使用 锁 ,进行加锁操作 ,保证共享资源的安全
执行可执行程序后,,发现tickets的值没有负数存在
设置为局部锁
锁要被所有线程看到
所以要定义一个类 TData 包含线程的名字 互斥锁对应的指针
表示线程创建时,要被传的参数
在主函数内部,通过 TData 类型new一个对象td,将公共的锁传递给所有线程
将对象td传递给自定义函数,作为参数args
在自定义函数上,通过对 对象内部的_pmutex的操作 完成加锁与解锁
通过访问对象内部的_name,来调用对应线程的名字
执行可执行程序符合预期,没有出现负数
4. 互斥锁细节问题
1. 访问同一个临界资源的线程,都要进行加锁操作保护,而且必须加同一把锁
(每一个线程在访问临界资源之前都要先加锁)
2. 每一个线程访问临界区之前,得加锁,加锁本质是给临界区加锁
加锁粒度尽量要细一些
3. 线程访问临界区的时候,需要先加锁 -> 所有线程都必须要先看到同一把锁 -> 锁本身就是公共资源
->锁如何保证自身安全? ->加锁和解锁本身就是原子的
(原子性:要么就不加锁,要加锁就加成功)
锁的申请是安全的,就可以保证锁保护的资源本身也是安全的
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,申请锁成功