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

2023-05-16

  当我们提到多线程、并发的时候,我们就会回想起各种诡异的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关键字?

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • postgresql insert into插入记录时使用select子查询

    span class token keyword INSERT span span class token keyword INTO span ls pos history span class token punctuation span
  • 怎么跟 HR 谈薪资?

    1 做功课 xff1a 提前了解应聘公司的行业薪酬水平 xff0c 公司岗位薪资范围 2 突专业 xff1a 面试电话接通的那一刻 xff0c 就要通过专业能力来为薪酬谈判做铺垫了 3 超预期 xff1a 面试中要总超出岗位需求展示你的全部
  • 量化策略研究员的成长策略

    量化策略研究员需要必备技能 xff1a 1 扎实的数学建模及严谨的逻辑推导能力 xff0c 充分掌握概率 统计等方法 xff1b 2 熟悉机器学习 深度学习 人工智能 xff0c Matlab Python C 43 43 xff1b 3
  • Mac安装MySQL

    目录 一 安装 二 环境变量 2 1 MySQL服务的启停和状态的查看 三 启动 四 初始化设置 4 1 退出sql界面 五 配置 5 1 检测修改结果 一 安装 第一步 xff1a 打开网址 xff0c https www mysql c
  • Win10下安装Ubuntu20.04双系统

    Ubuntu和Linux的区别 搞清楚Linux和Ubuntu的关系 xff1a 1 严格来说 xff0c Linux并不是操作系统 xff0c 而是一个操作系统的内核 xff0c 严谨一些可以说 xff1a linux 一般指 GNU 套
  • 面试的时候,如何自我介绍?

    就是自我介绍的时候 xff0c 应该先说些什么 xff1f 中间怎么说 xff1f 最后怎么结尾 xff1f 主要说那些内容比较好 xff1f 而且在纸质的个人简历之中也有 自我介绍 xff0c 那么两个 自我介绍 的内容是一样的吗 xff
  • 结果论的学习

    结果论 xff0c 是指主张一个行为的对错完全取决于此一行为所造成的结果 xff0c 所谓对的行为就是行为者在面临各种可能的行为选择时 xff0c 采行其中能达成最佳结果的行为 中文名 结果论 外文名 Consequentialethics
  • 如何修改oracle数据库字符集

    1 以管理员身份进入数据库 sqlplus nolog conn sys as sysdba password xxxxxxxx 2 修改字符集 SHUTDOWN IMMEDIATE STARTUP MOUNT ALTER SYSTEM E
  • 详解Redisson分布式限流的实现原理

    我们目前在工作中遇到一个性能问题 xff0c 我们有个定时任务需要处理大量的数据 xff0c 为了提升吞吐量 xff0c 所以部署了很多台机器 xff0c 但这个任务在运行前需要从别的服务那拉取大量的数据 xff0c 随着数据量的增大 xf
  • 聊一聊过度设计!

    文章目录 什么是过度设计 xff1f 过度设计的坏处如何避免过度设计充分理解问题本身保持简单小步快跑征求其他人的意见 总结 新手程序员在做设计时 xff0c 因为缺乏经验 xff0c 很容易写出欠设计的代码 xff0c 但有一些经验的程序员
  • 中国移动业务支撑系统简介(BOSS、BASS、BOMC、4A及VGOP)

    业务支撑系统 xff08 Business Support Systems xff0c 简称BSS xff09 主要应用于通信行业 xff0c 通过该系统对用户执行相应业务操作 它采用省中心 全国中心两级系统架构 xff0c 两级系统相辅相
  • 推荐系统遇上深度学习(一一一)-双重样本感知的DIFM模型

    上一篇中 xff0c 我们介绍了样本感知的FM模型 xff0c 也就是IFM模型 而本文将介绍其改进版本 xff0c 称为Dual Input aware Factorization Machine xff08 DIFM xff09 xff
  • Java高并发之CyclicBarrier简介

    Java 中的 CyclicBarrier 是一种同步工具 xff0c 它可以让多个线程在一个屏障处等待 xff0c 直到所有线程都到达该屏障处后 xff0c 才能继续执行 CyclicBarrier 可以用于协调多个线程的执行 xff0c
  • Linux xargs命令介绍

    Linux 中的 xargs 命令是一个非常有用的命令行工具 xff0c 可以将一些参数集合传递给其他命令作为参数 xff0c 并利用指定的命令进行处理 它可以帮助我们批量处理文件 xff0c 执行一些需要多个参数的命令 xff0c 并且支
  • 深入理解Spring的事件通知机制

    Spring作为一个优秀的企业级应用开发框架 xff0c 不仅提供了众多的功能模块和工具 xff0c 还提供了一种灵活高效的事件通知机制 xff0c 用于处理组件之间的松耦合通讯 本文将详细介绍Spring的事件通知机制的原理 使用方法以及
  • 人工智能未来是否会取代人类程序员?

    这个话题在近期来引起了很大讨论 xff0c 尤其是当GPT4发布后 xff0c 其展现出来的能力让很多岗位的从业者战战兢兢 xff0c 比如像程序员 xff0c 甚至有大佬跳出说三年 AI一定会取代程序员 人工智能和机器人是否会大规模取代人
  • 为什么说过早优化是万恶之源?

    Donald Knuth xff08 高德纳 xff09 是一位计算机科学界的著名学者和计算机程序设计的先驱之一 他被誉为计算机科学的 圣经 计算机程序设计艺术 的作者 xff0c 提出了著名的 大O符号 来描述算法的时间复杂度和空间复杂度
  • Linux parallel 命令使用手册

    文章目录 引言安装和配置GNU Parallel安装配置 GNU Parallel的基本用法GNU Parallel的高级用法1 在多个计算机上并行执行作业2 从文件中读取命令行参数3 生成详细的日志和报告 GNU Parallel的优缺点
  • 使用ffmpeg缩小视频体积的几种方式

    随着视频制作的普及 xff0c 视频文件的体积也越来越大 xff0c 给存储和传输带来了很大的困扰 为了解决这个问题 xff0c 我们可以使用FFmpeg这个强大的工具来缩小视频的体积 本文将介绍三种常用的方法 xff1a 调整视频的分辨率
  • 从CPU的视角看 多线程代码为什么那么难写!

    当我们提到多线程 并发的时候 xff0c 我们就会回想起各种诡异的bug xff0c 比如各种线程安全问题甚至是应用崩溃 xff0c 而且这些诡异的bug还很难复现 我们不禁发出了灵魂拷问 为什么代码测试环境运行好好的 xff0c 一上线就