线程互斥与同步

用户级线程 + 内核的LWP = Linux线程

OS概念中经常说的
用户级线程 和 内核级线程 也就是线程实现真的是在OS内部实现,还是应用层或用户层实现
很明显Linux是属于用户级线程

用户级执行流(用户级线程) :内核lwp = 1 : 1
也有1:n的但我们今天不管了

下面来谈谈线程库中的栈
在这里插入图片描述

这个栈不仅仅要简单的变量定义,入栈出战
每个执行流本质都是一条调用链
栈结构本质是为了支持应用层完成整个函数调用链所对应的
临时变量空间的开辟和释放
所以主线程当然要有自己的调用链
新线程在执行流上和主线程是独立的,所以他们形成调用链时
必定每一个人都要有自己独立的栈结构,让自己的调用链不受别人的
干扰,所以每一个线程都要有自己的栈结构。

站在线程角度每个线程都有自己独立的栈结构,但并不是说我
想访问你这个栈我就访问不了,其实有办法


创建多线程

把pthread_create套进循环里,每次形参的tid用vector来管理起来
也方便后续等待
在这里插入图片描述
创建线程给线程入口函数传参时,可不可以传入for循环中的临时变量?
在这里插入图片描述
肯定是不可以的,for循环结束 td变量也就释放了,你传的是临时变量的地址,你不能让线程指向主线程的栈而且还是一个临时变量,传参很大可能会翻车。

用全局变量传参呢?可能就不只需要定义一个因为是多线程

我们直接new出来一个数据对象给线程传参,这是比较原始的方法。
在这里插入图片描述
td变量指向的内容是堆空间的,每一次for循环都会重新创建堆空间
所以每个线程都可以访问属于自己的堆空间。

(也可以用容器,容器的本质不也是在堆上开辟吗,那套的层就多了,麻烦。)

从这个例子可以看出,所以线程一定是共享堆空间的,要不然我随便传给你一个堆空间上的对象怎么能传给你?
说明堆空间其实被大家线程共享,只不过我们一人一个罢了
我们不要互相干扰,如果今天我就是想让线程1访问线程2 的堆空间
能做到吗?绝对可以
我们把所有堆空间指针放到一个数组里保存起来,我不就可以访问了。
但我们不这么做。
我们一个线程拿着属于自己的一个指针就走了

进程内的每个线程本来就是一家人资源互相访问很正常。


在这里插入图片描述

所有的线程,执行的都是threadRoutine这个函数,这个函数就是被重入了。

问题:每个线程都要形成test_i这个临时变量,那每个线程都执行同一个函数那这个临时变量互相会影响吗?

在这里插入图片描述
图中看到每一个线程的test_i地址都不一样
验证出了每一个线程都会有自己独立的栈结构!

问题:如果我主线程就是想访问第一个线程的test_i这个变量,在技术上能做到吗?

答案是可以的,因为新线程栈区在库里,依旧在地址空间里面
如果主线程想访问任意线程栈区数据依旧能访问
验证一下
可以利用全局定义的变量,和线程内线程名配合拿到某个线程的test_i地址,这样主线程就可以通过这个全局变量拿到了
在这里插入图片描述

其实想说的就是
其实线程和线程之间,几乎没有秘密
线程的栈上的数据,也是可以被其他线程看到并访问的。
虽然能访问甚至是修改都是可以的,但我们禁止这么做,无论堆区还是栈区每个线程只有自己知道它的虚拟地址,每个线程把自己的资源保护好,不要让别人随随便便拿走就可以了

所以每个线程都有自己独立的栈结构,但不敢说私有的栈结构


如果定义一个全局变量,然后让所有线程去访问它并且++,
那这个代码有问题吗?
在这里插入图片描述

是有问题的。
这个全局变量g_val 被多个线程共享访问,这个g_val叫共享资源
共享资源被多线程并发访问就有可能导致数据不一致问题
这个问题稍后讨论

把问题聚到一下

线程的局部存储

如果我线程想要一个私有的全局变量呢????

在这里插入图片描述

利用编译选项 __thread来定义,这个编译选项不是c/c++提供的,而是编译器提供的一个编译选项
它只能定义内置类型,不能用来修饰自定义类型

被它修饰的全局变量是储存在 库里面tcb线程控制块中的局部存储中。
在这里插入图片描述

这样每个线程都有自己私有的全局变量,而且每个线程的这个全局变量都是一个变量名

那这个私有全局变量有啥用?
1.例如定义变量存储pid,而不用频繁调用系统调用getpid
2.在深度调用链函数调用时,,不用把局部定义的变量传入深度函数之中,而是可以直接用这个私有全局变量。

分离线程

在这里插入图片描述

默认新创建的线程是joinable 需要被等待的 属性
如果不Join就会内存泄漏,因为它的资源不会释放

如果今天根本不关心线程把任务完成的如何,还要让主线程能腾出手不必等待了,就用pthread_detach 分离
pthread_detach 分离后
1 . 如果一个线程已经被分离了,我们不能join了,也不需要Join了
,如果join就会返回错误码,等待失败
2. == 线程执行完毕后==,原生线程库会自动释放线程
在库里把库创建的tcb,栈,底层的轻量级进程就是pcb,全都释放掉
剩下的页表,地址空间,代码数据你别管,这是人家进程的。

这个分离的工作应该由谁来做呢?
既可以由主线程来做,也可由每个线程自己来分离
在这里插入图片描述

注意是线程执行完毕以后释放线程资源,而不是调用就释放

新线程中分离时,中间这个sleep如果不加,如果新线程还没分离主线程就开始等待,那么
会偶发看到等待成功的现象

细节

分离后线程本来是能执行个5秒,但是为什么代码只输出了这么一点?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

因为线程创建出来分离后,主线程立马开始join,分离后不能等待了,所以join直接出错立马返回,main函数也就跑完了所以进程也就结束了线程也没要结束。也就是主线程先退了

所以代码一定要保证主线程是最后退出,常见的是主线程永远不退出。

分离本质就是改线程是否是分离的属性
分离后线程依旧共享地址空间的资源,但是线程退出了和主线程没关系了

线程互斥

上面的话题 说过 定义一个全局变量,每个线程都能看到并修改这个变量,在默认情况下把这个全局变量叫做共享资源。
问题:
共享资源在被多线程并发访问时,有没有可能一个线程正在访问另一个线程就来读或者来写,进而因为另一个线程的读写修改会影响到我呢?
就会造成因为共享造成的数据不一致问题。

写一份代码,说一个场景,引入互斥的必要性

用多线程,模拟一轮抢票

有1000个座,就得卖1000张票,而不能卖出1001张

说白了就是一堆线程对全局变量做减减

肯定是票数大于0再去抢票减减,否则就是抢票完成线程退出
抢票是要花时间的,用usleep来充当时间花费
还要用Printf输出哪个线程抢到了哪个票

在这里插入图片描述
创建4个线程并发去访问同一个全局变量票数,大家都要对这个票数做减减
tickets就叫共享资源

在这里插入图片描述
最终发现抢票时竟然抢到了0,甚至-1,-2

为什么会出现这样的问题?
共享资源被多线程并发访问造成了数据不一致问题
造成这种问题肯定和多线程并发访问是有关系的
tickets- - 和判断 tickets > 0
无非每个线程执行代码时都要对变量做检测,做减减
有没有可能你正在做判断时,其他线程也来了
有没有可能你正在做tickets - - 时,其他线程也来了
多个线程都在并发访问,互相没有约束,你对ticktes做操作时,别人
也来访问了,就可能会造成数据不一致问题

我们先谈tickets - - 这个操作在多线程访问时是否是安全的问题
也就是对一个全局变量进行多线程并发 - - / ++操作是否是安全的

再来谈为什么票数会减到 - 2,虽然这两个关系不是直接相关,但是理解上面的问题,
这个现象也就能理解了。

并发访问全局变量++ – 肯定不安全

基本事实
在这里插入图片描述

你定义的全局变量 它的本质一定是在内存中的
初始值tickets设定为1000
问题
如果进行票数减减,它的本质是对数据做计算
要对数据做计算必须得在cpu内部计算
可是数据当前在内存里,
第一步必须得先将tickets读入到cpu的寄存器中
tickets–
1.先将tickets读入到cpu的寄存器中
2.CPU内部进行- - 操作
3. 将计算结果写回内存
这三步每一步都会对应一条汇编操作
tickets - - => 1. mov [内存某位置xxx] eax 2. - - 3. mov eax [ xxx ]

你在C语言上看到的一条语句,最终可能经过编译器编译会变成多条语句

多线程每一个线程都要减减,本质每一个线程都要执行这三条汇编语句

那这样带来的结果是什么呢?

现在假设有一个线程1,它要执行减减,读取变量的值1000到CPU寄存器中了
线程1第一步读取数据执行完
可是当前任何线程在任何两条代码执行的间隙期间,任何线程随时都有可能被切换
一个线程在被执行时,几乎在任何时间点,在任何的代码位置它都可能被切换
这个线程刚刚执行完第一步,正准备执行第二步
这里要暂停一下 线程进行调度执行时,每个线程都是CPU调度的基本单位,所以当它调度时
它一定要有自己的硬件上下文
当线程1执行完第一步之后,线程要被切换走了,所以它不是把自己的PCB拿走就完了,
它要把自己的上下文数据要保存起来,当然cpu寄存器的内容1000还在
讲切换时说过一句话,叫 寄存器不等于寄存器的内容
CPU寄存器只有一套,但是每个线程在运行期间它要使用CPU这一套寄存器,但当它走的时候他要把自己在CPU寄存器里保存的临时数据要带走,这叫做它的上下文
保存它上下文的本质是为了方便它后续进行恢复
所以寄存器只有一套,但每个线程都要有自己对应的寄存器对应的数据
这个数据每个线程都有。

数据从内存读到CPU寄存器的过程是拷贝的过程,原始内存中的数据还在,只不过
寄存器里又多了一份。

所以先要说一个重要结论
一个线程在执行时,把(共享数据tickets)数据从内存加载到CPU寄存器的本质是
把数据的内容,变成了自己的上下文 - - - 就是说 这个变量的数据以拷贝 的方式给自己(线程1)单独拿了一份

一旦我再执行期间把一个变量放到寄存器里了,从此往后这个变量其实就属于我自己了
直到我最后把减减操作做完,因为这个变量叫我的上下文
所以把数据放到寄存器里了,其实是把数据看起来放在大家都共享的寄存器里
但反而是这个变量变成我自己的上下文,以拷贝的方式我自己独占了一份

在这里插入图片描述

回过头再看,线程1执行了第一步把数据读取到CPU了,不好意思他被切换了,
一切换他要保存自己的上下文。
保存后就让其他线程来执行,此时线程2 就来了
它也要执行减减操作,第一步把数据读取到CPU当中,然后再减减,然后再写回
线程2 很幸运的是这三步它完成了,所以内存中tickets 1000 被改成999
线程2 在它的时间片内运气好 没人打扰到他,假设线程2 一直重复没人打扰的执行这三步

在这里插入图片描述
线程2 多次执行减减操作没人打扰,我们本来就是循环抢票,最后把内存tickets干到10
在这里插入图片描述
在这里插入图片描述
干到10后,线程2准备再做一次减减,刚完成第一步读取数据10到CPU寄存器中,读完10后第一步完成,正准备做第二步的时候,对不起,线程2 你的时间片终于到了
那怎么办呢?线程2 就把寄存器中自己上下文保存起来走了
在这里插入图片描述

此时线程1就被切换回来了,线程1切换回来的第一件事情并不是直接紧接着执行第二步,
而是恢复上下文,把自己曾经读取到的数据1000恢复到CPU寄存器里。
接着执行第二步减减把寄存器1000改为999,然后执行第三步把999写回内存中
在这里插入图片描述

完蛋了,人家线程2好不容易 把数据ticktes减到了10张,你现在直接干到999
也就是把别人做的工作全都覆盖了
这种情况就导致了tickets的数据不一致问题

所以最终结论,多线程并发访问共享资源减减操作不是安全的,或者叫做不是原子的。

理解了tickets - -
现在再来理解为什么抢票的结果为什么会出现负数
别忘了今天抢票不仅仅对tickets做了减减,还对tickets做了 if 判断呢

判断的过程 if (tickets > 0 )是数据运算的过程吗?
是运算,因为tickets >0 它叫做逻辑运算,(tickets- -叫做数值计算)

说到底你也是计算,所以 tickets >0 你也要把 tickets 和 0 读到寄存器里,然后在CPU内部判断tickets是否 大于 0 ,得到逻辑结果之后确定是否执行if代码块中的代码

今天假设tickets票值已经为1 了
当一个线程进入判断时,第二个线程,第三四个线程都有可能来判断
你刚刚判断条件成立,对不起线程被切换出去了
每一个线程都在判断读到tickets是1 被切走了

所以第一个线程进入判断代码块继续执行减减,就减到0了
减到0之后呢,第二个线程也来了,他也要执行减减,执行汇编的那三步,
上一个已经把票数减到了0,你在做减减操作,你进入判断的时候你以为票数是1,
当你被唤醒继续执行if中减减时,tickets被线程1减到了0,很尴尬你在做减减你要重新读数据,因为tickets- - 是分三步的。
所以票数已经是0 了,但你已经在循环里了所以再减减票数就变成-1 了
线程2 ,线程3 也一样
在这里插入图片描述
打印tickets 和 tickets - - 都要重新读取数据
不要以为判断读取到tickets是1后面就不读了
所以最终就出现了票数是负数
因为多线程并发访问时导致当前数据被操作时,导致数据同时被修改进而导致数据不一致

怎么解决呢?
分析清楚什么原因导致的,本质是就是正在对票数检测或修改时老是有其他线程打扰
本质上是我在读数据的时候你也来读了,这是时候就有问题,比如tickets - - 我在修改时你也来修改了,那我们两个互相干扰了
所以要做到
对共享数据的任何访问,保证任何时候只有一个执行流访问!----互斥!! ---------锁

补充说明

在这里插入图片描述

我们一直在说线程切换,线程什么时候切换?
时间片到了
可时间片到了你也得给我个时机啊,什么时候切换?
线程切换的时间点也是在内核到用户返回时做检测
线程从内核返回到用户时,它要做切换检查,不一定会切换
所以调用usleep时候
1.既可以保证当前线程被挂起 或 被唤醒 所以一定会经历从内核到用户的状态的切换
2.usleep过程中尽量让当前进程的时间片最后都会sleep,时间片一旦到了,
每个线程的步调创建到运行休眠的时间都是差不多的,所以切换的时间点就比较雷同了
所以一切换的话在tickets > 0 这个时间点它就有较大概率去线程切换了。

如果不加if判断 ,就只是票数 - -
有很低概率可能票数就像我们研究tickets- - 不安全时 票数会变大
但这种测试概率很低,基本测不出来
为什么加usleep和printf?
因为我们想通过这两句调用来尽可能让多线程执行时增加他们的并发度
和切换的概率
不然tickets - - 就只能祈祷时间片到了线程切换导致出问题

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值