从CPU的视角看 多线程代码为什么那么难写!

news2024/11/29 1:40:32

  当我们提到多线程、并发的时候,我们就会回想起各种诡异的bug,比如各种线程安全问题甚至是应用崩溃,而且这些诡异的bug还很难复现。我们不禁发出了灵魂拷问 “为什么代码测试环境运行好好的,一上线就不行了?”。 为了解决线程安全的问题,我们的先辈们在编程语言中引入了各种各样新名词,就拿我们熟悉的Java为例,不仅java语言自带了synchronized、volatile、wait、notify… ,jdk中各种工具包也是层出不穷,就比如单一个Lock,就可以有很多种实现,甚至很多人都谈锁色变。

  为什么会出现这种情况,我们得先从CPU和主存(RAM)的关系说起。 上个世纪80年代,PC机兴起的时候,CPU的运算速度只有不到1MHz。放现在你桌上的计算器都可以吊打了它了。那时候就是因为CPU运算慢,它对数据存取速度的要求也不那么高,顶多也就1微秒(1000ns)取一次数据,一次访存100ns对CPU来说也算不上什么。 然而这么多年过去了,CPU一直在沿着摩尔定律的道路一路狂奔,而内存访问延迟的速度却一直止步不前。(当然存储也有非常大的发展,但主要体现在容量方面,而访问延时自诞生初就没什么变化)。

  我们来对比下CPU和内存过去几十年之间的发展速率:
在这里插入图片描述

  可以看出,在过去40年里, CPU的运算速度增量了上千倍,而内存的访问延时却没有太大的变化。 我们就拿当今最先进CPU和内存举例,目前商用的CPU主频基本都是3GHz左右的(其实十多年前基本上就这个水平了),算下来CPU每做一次运算仅需0.3ns(纳秒)。而当前最先进的内存,访问延迟是100ns左右的,中间相差300倍。如果把CPU比作一个打工人的话,那么他的工作状态就会是干一天活然后休一年,这休息的一年里等着内存里的数据过来(真是令人羡慕啊)。

  其实CPU的设计者早就意识到了这点,如果CPU真是干1休300的话,未免也太不高效了。在说具体解决方案前,我这里先额外说下内存,很多人会好奇为什么主存(RAM)的访问速度一直上不来? 这个准确来说其实只是DRAM内存的速度上不了。存储芯片的实现方式有两种,分别是DRAM和SRAM,SRAM的速度其实也一直尽可能跟着CPU在跑的。那为什么不用SRAM来制造内存?这个也很简单,就是因为它存储密度低而且巨贵(相对于DRAM),所以出于成本考量现在内存条都是采用DRAM的技术制造的。

  SRAM容量小成本高,但速度快,DRAM容量大成本低,但速度慢。这俩能不能搭配使用,取长补短?结论是肯定的,在计算机科学里有个”局部性原理“,这个原理是计算机科学领域所有优化的基石。我这里就单从数据访问的局部性来说,某个位置的数据被访问,那么相邻于这个位置的数据更容易被访问。那么利用这点,我们是不是可以把当前最可能被用到的小部分数据存储在SRAM里,而其他的部分继续保留在DRAM中,用很小的一块SRAM来当DRAM的缓存,基于这个思路,于是CPU芯片里就有了Cache,CPU的设计者们觉得一层缓存不够,那就给缓存再加一层缓存,于是大家就看到现在的CPU里有了所谓的什么L1 Cache、L2 Cache, L3 Cache。

  存储示意图如下,真实CPU如右图(Intel I7某型号实物图):
在这里插入图片描述

  多级缓存的出现,极大程度解决了主存访问速度和CPU运算速度的矛盾,但这种设计也带来了一个新的问题。CPU运算时不直接和主存做数据交互,而是和L1 Cache交互,L1 cache 又是和L2 Cache交互…… 那么一定意味着同一份数据被缓存了多份,各层存储之间的数据一致性如何保证? 如果是单线程还好,毕竟查询同一时间只会在一个核心上运行。但当多线程需要操作同一份数据时,数据一致性的问题就凸显出来了,如下图,我们举个例子。
在这里插入图片描述
  在上图中3个CPU核心各自的Cache分别持有了不同的a0值(先忽略E和I标记),实际上只有Cache0里才持有正确的数值。这时候,如果CPU1或者CPU2需要拿着Cache中a0值去执行某些操作,那结果可想而知。如果想保证程序在多线程环境下正确运行,就首先得保证Cache里的数据能在"恰当"的时间失效,并且有效的数据也能被及时回写到主存里。

  然而CPU是不知道当前时刻下哪些数据该失效、哪些该回写、哪些又是可以接着使用的。这个时候其实CPU的设计者也很犯难,如果数据频繁失效,CPU每次获取必须从主存里获取数据,CPU实际运算能力将回到几十年前的水平。如果一直不给不失效,就会出现数据不一致导致的问题。于是CPU的设计者不干了:”这个问题我处理不了,我给你们提供一些可以保证数据一致性的汇编指令,你们自己去处理”。 于是大家就在intel、arm的开发手册上看到了像xchg、lock、flush……之类的汇编指令,C/C++语言和操作系统的开发者将这些封装成了volatile、atomic……以及各种系统调用,JVM和JDK的开发者又把这些封装了我在文首说的那一堆关键词。 于是CPU的设计者为了提升性能导致数据一致性的问题,最终还是推给了上层开发者自己去解决。

  作为上层的开发者们(比如我们)就得判断,在多线程环境下那些数据操作必须是原子操作的,这个时候必须使用Unsafe.compareAndSwap()来操作。还有那些数据是不能被CPU Cache缓存的,这个时候就得加volatile关键词。极端情况下,你可以所有的操作搞成原子操作、所有的变量都声明成volatile,虽然这样的确可以保证线程安全,但也会因为主存访问延时的问题,显著降低代码运行的速度。这个时候局部性原理又发挥出其神奇的价值,在实际情况下,绝大多数场景都是线程安全的,我们只需要保证某些关键操作的线程安全性即可。举个简单的例子,我们在任务向多线程分发的时候,只需要保证一个任务同时只被分发给一个线程即可,而不需要保证整个任务执行的过程都是完全线程安全的。

  作为Java开发者,Java和JDK的开发者们已经帮我们在很多场景下封装好了这些工具,比如我们就拿ReentrantLock实现一个多线程计数器的例子来看。
在这里插入图片描述

  其中increment() 本身不是一个线程安全的方法,如果多个线程并发去调用,仍然会出现count值增长不准确的问题。但在lock的加持下,我们能保证increment()方法同时只能有一个线程在执行。想象下,如果我们把上述代码中的counter()方法换成一些更复杂的方法,而完全不需要在方法中去考虑线程安全的问题,这不就实现了仅在关键操作上保证准确性就能保证全局的线程安全吗!而当我们去深究lock的实现时,就会发现它底层也只是在tryAcquire中使用CAS设置了state值。
在这里插入图片描述
  在多线程编程中,加锁或加同步其实是最简单的,但是在什么时候什么地方加锁却是一件非常复杂的事情。你需要考虑锁的粒度的问题,粒度太大可能影响性能,粒度过小可能导致线程安全的问题。还需要考虑到加锁顺序的问题,加锁顺序不当可能会导致死锁。还要考虑数据同步的问题,同步的数据越多,CPU Cache带来的性能提升也就越少……

  从上面CPU的发展变化我们可以看到,现代CPU的本质其实也是一个分布式系统,很多时候仍需要编程者手动去解决数据不一致性的问题。当然随着编程语言的发展,这些底层相关的东西也逐渐对普通程序员变得更透明化,我们是不是可以预想,未来是不是会有一门高性能、并且完全不需要程序员关注数据一致性的编程语言出现?

  最后上面计数器代码给大家留一个思考题: 代码中的counter变量声明是否需要加volatile关键字?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/474199.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何将 github pages 迁移到 vercel 上托管

如何将 github pages 迁移到 vercel 上托管 前言 早期网站使用 github pages,后来迁移到 coding,最近又放到腾讯云网站静态托管,无论是 coding 的 cos 存储桶,还是静态网站托管 他们都是收费的,那有没有免费的托管商呢,既不影响网站的访问速度还免费,于是,找了一下,还真有,ve…

电商广告营销中,有哪些常见公式和优化手段

对于电商广告ROI的计算,广泛地应用在二类电商、跨境电商等营销场景中,了解这些相关的投放公式,有助于我们更好地进行广告投放和优化。 特别是针对商家而言,可以指导选品及定价策略、优化最终的广告投放收益,避免出现花…

学系统集成项目管理工程师(中项)系列14_采购管理

1. 概念和术语 1.1. 采购是从项目团队外部获得产品、服务或成果的完整的购买过程 1.2. 三大类 1.2.1. 工程 1.2.2. 产品/货物 1.2.3. 服务 2. 主要过程 2.1. 编制采购管理计划 2.2. 实施采购 2.3. 控制采购 2.4. 结束采购 3. 合同 3.1. 包括买方和卖方之间的法律文…

css布局实战:动态详情九宫格

上篇文件介绍了如何实现九宫格图片展示:css布局入门级实战之九宫格网格布局.不过存在一个问题:图片之间没有间距,用户体验欠佳;基于以上问题,本文进行优化. 较之前实现样式做以下调整:四张图按照两行显示,每行显示三个.不足的显示空白.之前是两行两列显示. 对应九宫格样…

忆暖行动|“他一个人推着老式自行车在厚雪堆的道路上走,车上带着学生考试要用的司机”

忆暖行动|“他一个人推着老式自行车在厚雪堆的道路上走,车上带着学生考试要用的sj” 一头白发,满山青葱 在那斑驳的物件褶皱中,透过泛黄的相片,掩藏着岁月的冲刷和青葱的时光。曾经的青年早已经不复年轻,但是那份热爱…

从信息泄露到权限后台

声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。 点点关注不迷路,每周不定时持续分享各种干货。 众亦信安,中意你啊&a…

1210. 穿过迷宫的最少移动次数

你还记得那条风靡全球的贪吃蛇吗? 我们在一个 n*n 的网格上构建了新的迷宫地图,蛇的长度为 2,也就是说它会占去两个单元格。蛇会从左上角((0, 0) 和 (0, 1))开始移动。我们用 0 表示空单元格,用 1 表示障碍…

题目 3166: 蓝桥杯2023年第十四届省赛真题-阶乘的和--不能完全通过,最好情况通过67.

原题链接: 题目 3166: 蓝桥杯2023年第十四届省赛真题-阶乘的和 https://www.dotcpp.com/oj/problem3166.html 致歉 害,首先深感抱歉,这道题还是没有找到很好的解决办法。目前最好情况就是67分。 这道题先这样跳过吧,当然以后还…

RabbitMQ 03 直连模式-可视化界面

这里先演示最简单的模型:直连模式。其结构图为: 一个生产者 -> 消息队列 -> 一个消费者 生产者只需要将数据丢进消息队列,而消费者只需要将数据从消息队列中取出,这样就实现了生产者和消费者的消息交互。 创建一个新的实验…

CLion开发工具 | 05 - 使用CLion开发ESP32

专栏介绍 一、准备工作 电脑上安装好ESP-IDF环境本文参考Jetbrains官方视频教程:在 Windows 上用 CLion 开发 ESP32 | CLion教程 | 嵌入式开发 | IDE 二、打开工程 复制一份新的helloworld工程。 使用CLion打开该工程。 选择信任该工程。 CLion打开后自动打开…

Linux 基础语法 -2

如果我们以后再Linux当中 写了一些命名,导致程序我们不能进行操作了,如这个死循环: 他就会一直输出 "hello Linux" ,我们就使用 ctrl c 来终止因为程序或者指令异常,而导致我们无法进行指令输入&#xff…

2023/04/27~28 刷题记录

D - JoJos Incredible Adventures 大致题义: 有一串由 0,1 构成的字符串,每次循环右移一位,行编号从 0 一直到 n-1。求这些行里由 1 构成的最大矩形面积。 题解: 我们其实可以观察到一串连续的 1 经过右移后是会形成一对正三角和…

C# WebService的开发以及客户端调用

目录 1、WebService简介 1.1 什么是XML? 1.2 什么是Soap? 1.3 什么是WSDL? 2、WebService与WebApi的区别与优缺点 2.1 WebService与WebApi的区别: 2.2 WebService的优缺点: 2.3 WebApi的优缺点: 3…

ShardingJDBC 数据库分片 流式处理+归并排序 优化原理刨析

1. 分库分表下的分页查询 业务数据达到一定数据量时,必定会引入数据库分片,但当对于分片的情况下,分页查询是如何做到的? 比如: 数据库db1,中有三个user表,user_0,user_1,user_2&a…

FreeRTOS 信号量(五) ------ 递归互斥信号量

文章目录 一、递归互斥信号量简介二、创建互斥信号量1. xSemaphoreCreateRecursiveMutex()2. xSemaphoreCreateRecursiveMutexStatic() 三、递归信号量创建过程分析四、释放递归互斥信号量五、获取递归互斥信号量六、递归互斥信号量使用示例 一、递归互斥信号量简介 递归互斥信…

Linux学习[8]文件权限深入2 默认权限umask SUID/SGID/SBIT file指令

文章目录 前言1. 默认权限umask1.1 默认权限含义1.2 查看默认权限1.3 默认权限在文件与目录的异同 2. 特殊权限2.1 SUID2.2 SGID2.3 SBIT2.4 SUID/SGID/SBIT 权限设置2.4.1 数字法2.4.2 符号法 3. file指令 前言 在linux学习6里面,通过ls -al命令列出文件的所有详细…

2023/4/30周报

目录 摘要 论文阅读 1、题目和现存问题 2、知识空间理论、GRU和自注意力机制 3、模型构建 4、实验准备 5、实验结果 深度学习 1、GRU 2、代码实例 3、GRU和GCN结合的可能性 总结 摘要 本周在论文阅读上,阅读了一篇基于自注意力机制和双向GRU神经网络的…

【MATLAB数据处理实用案例详解(17)】——利用概念神经网络实现柴油机故障诊断

目录 一、问题描述二、利用概念神经网络实现柴油机故障诊断原理三、算法步骤3.1 定义样本3.2 样本归一化3.3 创建网络模型3.4 测试3.5 显示结果 四、运行结果五、完整代码 一、问题描述 柴油机的结构较为复杂,工作状况非常恶劣,因此发生故障的可能性较大…

linux常用命令大全

作为开发者,Linux是我们必须掌握的操作系统之一。因此,在编写代码和部署应用程序时,熟练使用Linux命令非常重要。这些常用命令不得不会,掌握这些命令,工作上会事半功倍,大大提高工作效率。 一. 文件和目录…

python使用迭代法验证角谷猜想

def fun(n):print(n)while n ! 1:n 3 * n 1 if n % 2 else n / 2print(finished)for i in range(2,1000):fun(i) 1、了解了什么是"角谷猜想" 对于任意一个自然数n,若n为偶数,则将其除以2;若n为奇数,则将其乘以3,然后再加1。如此经过有限次运算后,总可以得到自然数…