前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Linux】线程互斥

【Linux】线程互斥

作者头像
lovevivi
发布2023-10-17 08:57:38
1590
发布2023-10-17 08:57:38
举报
文章被收录于专栏:萌新的日常

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-10-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 背景概念
    • 多个线程对全局变量做-- 操作
    • 2. 证明全局变量做修改时,在多线程并发访问会出问题
    • 3. 锁的使用
      • pthread_mutex_init
        • pthread_metux_destroy
          • pthread_mutex_lock 与 pthread_mutex_unlock
            • 具体操作实现
              • 设置为全局锁
            • 设置为局部锁
            • 4. 互斥锁细节问题
            • 5. 互斥锁的原理
              • 背景知识
                • 具体实现
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档