惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
爱范儿
爱范儿
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 叶小钗
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - Franky
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
罗磊的独立博客
博客园 - 聂微东
T
Troy Hunt's Blog
美团技术团队
IT之家
IT之家
A
Arctic Wolf
腾讯CDC
雷峰网
雷峰网
SecWiki News
SecWiki News
博客园_首页
L
LINUX DO - 最新话题
Cloudbric
Cloudbric
量子位
N
News and Events Feed by Topic
小众软件
小众软件
C
CXSECURITY Database RSS Feed - CXSecurity.com
Cyberwarzone
Cyberwarzone
J
Java Code Geeks
V
V2EX
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Latest news
Latest news
Webroot Blog
Webroot Blog
F
Fortinet All Blogs
P
Privacy International News Feed
NISL@THU
NISL@THU
Google Online Security Blog
Google Online Security Blog
WordPress大学
WordPress大学
PCI Perspectives
PCI Perspectives
GbyAI
GbyAI
宝玉的分享
宝玉的分享
阮一峰的网络日志
阮一峰的网络日志
S
Secure Thoughts
Simon Willison's Weblog
Simon Willison's Weblog
P
Palo Alto Networks Blog
V
Visual Studio Blog

XINDOO

关于内卷,几个值得深想的洞察 当创作被 Skill 化:我用 AI 写了一部 320 章的长篇网文 AI第一剑,先斩程序员 AI 也会偷懒?这个 PUA 工具专治“摸鱼式编程” Gstack 深度解析:YC CEO 开源的 AI 工程团队 GitHub Trending霸榜!深度解析AI Coding辅助神器 Superpowers 深度探讨:从 OpenClaw 爆火,看 AI Agent 的真相与程序员的未来 我复刻了NotebookLM的信息图功能 我用AI写了部小说,这里是整个过程 [翻译]我在谷歌14年学到的21堂课 2万字吊打40万字:为什么我的“牛马Agent”比“数字分身”更聪明? 最近AI领域爆火的 Agent Skills 是什么? 从计算机科学的视角来看拖延症 一周改6个库后我悟了:AI时代,程序员正在退化成“甲方” 使用n8n做一个自动同步更新的Github项目问答机器人 Agent设计模式——第 20 章:优先级排序 Agent设计模式——第 1 章:提示词链 Agent设计模式——附录 D - 使用 AgentSpace 构建 Agent Agent设计模式——第 19 章:评估和监控 Agent设计模式——第 10 章:模型上下文协议 (MCP) Agent设计模式——第 21 章:探索和发现 Agent设计模式——智能体设计模式 Agent设计模式——第 11 章:目标设定和监控 Agent设计模式——第 9 章:学习和适应 Agent设计模式——第 16 章:资源感知优化 Agent设计模式——附录 G - 编码 Agent Agent设计模式——第 13 章:人机协同 Agent设计模式——第 17 章:推理技术 Agent设计模式——附录 F - 深入剖析:Agent 推理引擎的内部运作机制 Agent设计模式——章节目录 Agent设计模式—— Agent设计模式——第 7 章:多 Agent 协作 Agent设计模式——附录 E - 命令行界面中的 AI Agent Agent设计模式——附录 C - Agentic 框架快速概览 Agent设计模式——第 3 章:并行化 Agent设计模式——**常见问题解答:Agentic 设计模式** Agent设计模式——第 14 章:知识检索(RAG) Agent设计模式——第 18 章:Guardrails/安全模式 Agent设计模式——第 15 章:Agent 间通信(A2A) Agent设计模式——第 8 章:内存管理 Agent设计模式——第 12 章:异常处理和恢复 Agent设计模式——第 4 章:反思 Agent设计模式——附录 B - AI Agentic 交互:从图形界面到现实世界环境 Agent设计模式——第 5 章:工具使用(函数调用) Agent设计模式——结论 Agent设计模式——第 6 章:规划 Agent设计模式——第 2 章:路由 从经验主义到贝叶斯理论:如何排查线上问题 我用AI为自己造了一把安全的开发者“瑞士军刀”” 从LLM和MCP的协同过程看如何做优化 打通Dify与AI工具生态:将Workflow转为MCP工具的实践 一文了解知识库背后的技术RAG AI应用的五个级别:从入门到专家的进阶之路 一文入门AI圈最近爆火的MCP协议 HTTP/3:性能改进(第 2 部分) deepseek-r1祛魅:从过度热捧到理性认知⁠ 为什么AI智能体需要工作流 如何用GPT-4o解读视频 json命令行处理神器jq介绍 OpenAI的结构化浅析 从大模型的原理到提示词优化 从经济学原理看团队分工合作 [翻译]关于人工智能的30个思考 从马斯洛需求层次理论谈职场激励 知识与智慧 如何使用大语言模型绘制专业图表 两个开源项目打造自己的大模型聚合平台 我让gpt4o给我推荐了一千多次书 得到了这些数据 用Langchain创建一个可以总结网页内容的Agent 推荐一个好用的命令行工具ShellGPT 关于ffmpeg height not divisible by 2的错误 使用Certbot解决https证书自动更新的问题 Spring Cache简明教程 软件开发中的抓大放小vs极致细节思维 OpenAI Assistants-API简明教程 OpenAI的多函数调用(Multiple Function Calling)简介 如何使用ffmpeg制作透明背景的视频 spring-kafka中ContainerProperties.AckMode详解 如何在地图上寻找最密集点的位置? IO密集型服务提升性能的三种方法 职场中的基本归因错误和自利归因 使用javax.validation.constraints校验参数合法性 Java Optional:让你的代码更优雅 ChatGPT函数调用初体验:让ChatGPT具备抓取网页文本的能力 如何使用ChatGPT提升自己的“码”力? 使用ffmpeg拼接两张图片 ThreadPoolExecutor——高效处理并发任务的必备良器 使用ffmpeg缩小视频体积的几种方式 Linux parallel 命令使用手册 为什么说过早优化是万恶之源? Linux xargs命令介绍 深入理解Spring的事件通知机制 Java高并发之CyclicBarrier简介 聊一聊过度设计! 详解Redisson分布式限流的实现原理 Java中使用HashMap时指定初始化容量性能一定会更好吗? 如何用ffmpeg截取视频片段&截取时间不准确的坑 XINDOO的2022年年终总结 使用ffmpeg将视频转成HLS(m3u8)格式 谷歌Guava LoadingCache介绍
从CPU的视角看 多线程代码为什么那么难写!
xindoo · 2023-04-30 · via XINDOO

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