【Linux】线程互斥

2023-10-17 08:57:38 浏览数 (1)

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,申请锁成功

0 人点赞