感觉现有的回答都不全面。
cpp原子库有些实现是调用操作系统提供的接口,所以这个实现可能跟操作系统有关。同时,又因为操作系统的实现是依赖于硬件的,所以具体的锁的实现要取决于硬件的支持情况。
题主有一个误区,因为原子操作一定需要关调度,关中断,这个理解是错的。锁的本质是同步数据,理论上说,只要保证特定的数据不被修改即可。在硬件层面上看,就是特定的物理内存,特定的cache line,不被修改,所以,并不是所有原子操作,都一定要关调度,关中断。
大多数主流的CPU,都会提供硬件指令,原子修改某个内存,对于Intel的CPU,手册system programming guide的8.1.2.2 Software Controlled Bus Locking里有详细的描述:
The bit test and modify instructions (BTS, BTR, and BTC).
The exchange instructions (XADD, CMPXCHG, and CMPXCHG8B).
The LOCK prefix is automatically assumed for XCHG instruction.
The following single-operand arithmetic and logical instructions: INC, DEC, NOT, and NEG.
The following two-operand arithmetic and logical instructions: ADD, ADC, SUB, SBB, AND, OR, and XOR.
如果操作系统使用的是硬件指令实现原子操作,那么用这些指令就可以了,这种实现不需要锁,不需要关中断或者调度。
对于ARM/PPC/RISCV也有类似的指令。
但是,也有例外情况。
比如ARM32/PPC32/RISCV32上不支持对8B数据的原子操作,只有Intel在32位环境中提供了CMPXCHG8B的指令(甚至于,Intel还提供了CMPXCHG16B的指令),这种情况就比较麻烦了。
atomic库里是提供了各种长度的数据的,如果硬件本身不支持,那么就需要通过软件实现。atomic库里有std::atomic_is_lock_free来告诉应用程序,这个操作是不是lock free的,如果不是,那么底层就是用锁来实现的。具体可以参考这个:
如果不是lock free的,那么其实它的实现跟用户自己用锁来实现是差不多的,甚至性能还不如用户自己实现。通过软件实现的原子操作是需要关中断,关调度,使用mem fence等动作保证数据不被改变,如果是用户自己的代码,确认中断不会更改关键数据的话,那么可以不用关中断。
某些开源库并没有考虑到不同硬件上的原子操作差异,所以在某些平台上,使用了原子操作的开源库实际上是会有风险的(比如openmp的kmp库在PPC/RISCV上)。
所以,原子操作可能是虚假的(软件模拟),也可能是真实的(硬件指令),在X86平台上,基本上都是真实的,在非X86平台,有可能是虚假的。
这个问题要仔细说明白的话,不简单。但广泛给个四海而皆准的回答就是:std::atomic确实是都用“锁”的——区别无非是哪种“锁”而已。
扩展一点说:
首先,std::atomic可以特化为不同类型,平台上如果直接提供了原子指令的类型,会使用源自指令来实现(这在很多人眼中,这就是“无锁”了)。没有直接提供的类型,会直接用锁来实现。
然后,问题就退化为:原子指令到底是不是真的“无锁”。实际上在逻辑上,原子指令本身在逻辑上,也是一种“锁”的实现。
接着,简单解释一下原子指令里的“锁”:
早期的双U/双核年代,原子指令是需要在多CPU之间额外支持的。它的基本原理是当进入原子指令时,会锁住整个总线,使之进入“原子状态”(没记错应该还会关掉硬中断)。
现在核心多了,再动辄锁总线就性能损耗太大了,于是就变为锁cache。对应内存的cache块的标记为独占(加锁),等到操作结束才会解除(解锁)。而这时候,抢锁的操作实际上是由电路实现的(类似于抢答器之类的电路)。
最后,原子指令在理论上已经达到了最优解了,虽然肯定还是无法避免性能损失(抢不到锁的脏页要放弃后重新加载),但肯定比在软件层做得要更好了。
是真正的“原子”。只不过,你严重误解了原子这个词。
一个变量是原子的,意思并不是整个程序对这个变量的八万次读写是一个整体。没这回事,不要和数据库事务的原子性混淆。
变量是原子的,意思是每次操作这个变量,包括读取其值、更改其值,这次操作中执行的唯一一条指令是原子的(对于面向对象语言,也可以是一次接口调用是原子的,这个后面会讲到)。
正因此,很多CPU才不得不专门提供一条Test and Exchange指令,把“检测内容,等于xx则修改其值”这样必须两三条指令才能完成的操作整合进一条指令,且在执行时锁定总线。
如果只靠总线锁定前缀的话,这个test and exchange操作先后执行的几条指令,虽然每一条都是原子的,但加起来就不是了。也就是这里必须提供一个方法,用来把几条指令绑起来(的确有CPU允许你在连续执行多条指令期间锁定总线,从而灵活组合一堆指令、让它们加起来是原子的。但这类操作往往需要很高特权级,不是用户态应用可以用的)。
注意这里有个思想:锁操作代价高昂,因此锁的粒度一定要小。最好小到只有一条指令,那么锁操作的代价就完全可控了。
注意,一条指令也是需要取指、译码、执行、写回结果等许多步骤的。
普通指令,这些步骤可以打断,可以相互穿插。比如,字节不对齐时,一条指令可能需要两次以上访存才能把待操作的数字载入寄存器,如果两次访存之间被打断、且另一条指令改写了被操作数字时,这里就会出现脏读(脏写也类似)。
CPU可以智能的分析指令执行涉及到的东西,从而避免几条指令穿插影响。但这个智能并不太高。比如,操纵和CPU字长相等的、字节对齐的int时,很多CPU可以保证原子性。
没错,不用锁总线,天然支持,只要你保证它的地址对齐满足CPU要求。但你要自己查阅资料来确认这一点,同时还要确认你的编译器生成的代码能够保证地址对齐。
那么这时候,cpp的atomic实际上等于什么都没做。
当然了,经常的,仅仅一条甚至七八条指令的原子性也是不够用的。比如,电商开发中经常遇到的,用户购买流程,需要先锁库存,下订单,等待付款,付款完成再把商品标记为出库:这一整套成千上万条指令都必须是原子的。
CPP的atomic不能也不该来保证这一点。
还记得吗?锁的粒度要尽量小。所以CPU努力把它做到了一条指令。
但是,电商这种情况该怎么办?
很简单,用一个atomic变量来保护数据。
注意了,CPU只能保护这个atomic变量不脏读脏写,它可保护不了你要保护的一大片数据在千百条指令执行过程中的原子性。
那该怎么办?
简单,想象一条虚拟的总线,当函数a执行时,锁住这条虚拟总线;执行完了,再给这条虚拟总线解锁。
比如,我们可以把数据库连接看作这条总线,然后把一个atomic变量等于1看作锁定状态,等于0则是free状态:那么,只要每个人都确认这个锁的从0到1是自己做的(也就是持有这个锁),那么就可以去修改数据库了。注意改完了要把锁恢复到0哦。
类似的,只要通过接口或者别的什么(甚至是文档中的约定)确保一切访问之前都要举行一个“锁定虚拟总线”的仪式,假装自己真的是通过这条总线访问的,那么当然也能达到原子访问的目的。
这就是为什么很多库会告诉你“随便用!我保证线程安全”的原因:很简单,它把必要的仪式都写死在接口实现代码里了。
注意,你的确得到了原子性/线程安全,执行时但并没有真的锁物理总线(我在脑中锁虚拟总线,关你物理总线什么事)。
只除了那条至关重要的test and exchange执行时。
仔细阅读、提炼这段描述,你才会理解为什么test and Exchange是必要的、为什么没有它就没办法正确实现锁。
当然,锁数据库链接粒度实在太大,这等于把数据库变成串行的了。实践中,数据库内部替你维护了表(级)锁、行锁等更小粒度的锁,甚至还区分读写锁。。。
类似的,电商购物,你也不要真的从用户下订单到支付完成都锁定数据库表或者表中的一行。不合适的使用大粒度锁是专业水平欠佳的表现。
相反,你要审慎思考:占用库存究竟是”锁住数据库中的一行(从而独占的修改它)”呢,还是“正确的把数据库余额减一(这个操作需要原子性),然后记住这个临时扣除状态,在用户付款后把状态改为永久、或者在用户付款后把余额再加一”:前者带来极长时间的数据库锁定,而后者只需要修改余额这个动作本身是原子性的。
这就是锁的优化。
前些年很是流行过一阵“无锁编程”,其核心思想就是不用任何大粒度锁,而是把一切同步操作都压缩到一条CPU指令。
换句话说,无锁编程并不是真正的无锁,而是锁粒度的极致优化,优化到只剩一条test and exchange指令。
换句话说:一切复杂操作的原子性、最终都可以归结为“一个guard变量”的原子性。
再换句话说,每次访问之前,都先围绕着guard举行一套仪式,就可以保证另外一堆复杂数据的原子性。这套仪式的关键,最终都可以归结到一条test and exchange指令。
能否认识到这一点,是你能否理解你的问题的必要前提(也是能否看出那群“一提12306,就说锁库存很可怕”的人的成色的前提)。
换句话说,真的理解了这里提到的东西,你才会明白自己的问题错在哪里。
似乎很少看到在讨论 atomic 的时候同时讨论 interrupts。我认为这两者虽然都直接与 CPU 有关,但是是可以分开讨论的,因为,虽然你每敲一下键盘、移动一下鼠标 CPU 都会被中断,但中断处理例程(Interrupt Service Routine,IRS)一般位于 driver 里[1],并且 CPU 从中断恢复之后,用户态程序的环境跟之前是一样的,而且用户态程序也无法直接操作 IF 来禁止被中断[2],只有 OS 核心才行。
关于是否会使用锁机制的问题,虽然我不是硬件方面的专家不过据我了解,比如 atomic 在 x86 上最终可能以类似 lock cmpxchg8b
这样的指令来实现。这个 lock 前缀意味着锁,目的是保证在其修饰的指令执行时对 shared memory 的独占,它可能会锁住 BUS,但如果这块内存在 CPU 里面已经 cache,那么只需要锁 cache 就可以了。
Beginning with the P6 family processors, when the LOCK
prefix is prefixed to an instruction and the memory area being accessed is cached internally in the processor, the LOCK# signal is generally not asserted. Instead, only the processor’s cache is locked. Here, the processor’s cache coherency mechanism ensures that the operation is carried out atomically with regards to memory.
atomic 是 CPU 实现的,所以,是的,在不同的 CPU 上实现肯定有差别。在讨论 atomic 的时候,我们更多关注是内存对齐,内存一致性,内存读写顺序的问题。
是真正的原子,直接对应到编译器的原子intrinsics,对应到CPU的相应指令。
都不是。你对原子的理解有偏差。
atomic是否用锁:可以通过atomic::is_lock_free和is_always_lock_free来判断。一般对于一些基本类型来说都不会用互斥锁,你传一个比较大的自定义类型进去大概率就得用锁了
atomic的实现:对于不用的cpu体系结构来说确实是不同的,比如你说的原子自增操作,对x86来说一个lock前缀就搞定了,对于其他体系结构可能需要在循环中使用ll/sc指令对或者cas操作来做一些争抢。
无论是lock前缀还是循环式的争抢,都只是指令层面的动作,与操作系统无关,不会禁用操作系统对其他线程的调度,一般只是锁住总线来做一个原子的读取和更新(在不使用ring bus而使用其他互联网络的系统中会更复杂一些),其他核上的线程还是在正常运行,只是可能因为总线被占用而某条指令卡住一小会。在使用ring bus的处理器中,实际上即使你不用atomic,只是单纯做一个对齐的写操作,总线也会被你占住一段时间, 这种占住总线的行为正是缓存一致性的serialization特性的要求
首先这是Fed一月 memo
先说结论:
FOMC 维持利率在 0-0.25% 不变。且确定 3 月完全停止 QE,同时 3 月加息也是箭在弦上,基本会后声明皆符合市场预期,没有太多的意外。
Powell 记者会确实是偏一点点的小鹰派,但我也认为,Powell 的说法不至于拉升市场加息预期至 5次 、并拉升缩表预期至上半年,反而比较像是在强化加息 4 次之预期。
另外我个人觉得,一些中文媒体似乎误读了Powell 记者会的部分片段,下面 Allen 再进一步说明。
1. 3 月加息停止 QE 早已定价
本次会议 Fed 再次确认 3 月将准备第一次加息,并同时停止 QE。
Fed 也再次重申,货币政策是要支持美国经济达到充分就业、与通膨长期均值维持 2.0% 的两大目标。
这部分我想市场早已定价,这裡完全不会是问题,所以我们不讨论太多。
2.未来加息在每次会议都可能发生 (?)
Powell 的原文说法是:Won't Rule Out Hike Every Meeting.
但我有看到部分中文媒体写:不排除每次会议都加息的可能性。
上述我想或许是误读了 (还是其实是我自己误会中文的意思 ?)
我的理解是:Powell 是说加息在未来每场会议都可能发生,指的是“不会在特定月份才加息”,不是说每场都要加息。
Powell 说得很合理,经济本来就是动态的,加息本就不会侷限在什麽月份才启动,端看当时的经济状况而定。
我认为Powell 上述说法,并未延展今年加息预期至五次或更多,若有这种想法,那绝对是误读了。
3.更大规模的缩表?
Powell 在记者会上提到,Fed 需要更大规模的缩表,但请大家不要恐慌,因为我又觉得部份中文媒体过度解读了。
我认为Powell 说到的“更大规模缩表”,在思维上指的是:
因为当前 Fed 资产负债表高达 8.9 万美元,这是新冠疫情爆发之前的两倍大,显然在绝对规模上是非常巨大的。
而上一轮 2017-2019 年 Fed 缩减资产负债表,是自 4.4 万亿美元缩到 3.7 万亿美元停止,缩表的幅度大概是 15.9%,共缩减了约 7000 亿美元。
确实每次缩表的经济背景绝对是不一样的,所以幅度也绝对不会相同,但我们随便抓,假设本轮缩表将缩减 10% 资产负债表规模,那麽这也要降低 8900 亿美元,规模当然很大。
但我认为,不需要过度恐慌在“更大规模缩表”这几个字上。更重要的,我认为是“Fed 缩表的速率是多少?”
我相信缩表没问题,缩表太快才是问题,因为缩表速度若太快,将直接影响的会是美债殖利率升速、以及殖利率曲线的斜率。
这点Powell 也非常清楚,Powell 在记者会上也不断强调,联准会内部尚未具体讨论到一切缩表的进度,要等到 3 月再说。
4.缩表比较可能落在下半年
Powell 在记者会上说明,希望在加息至少一次之后,再来开会讨论缩表的事情,且委员会至少将讨论一次,才会做最终拍板。
更重要的,Powell 希望缩表的进程是有秩序的、是可被预见的过程。
从上述Powell 丢出的时间表看,我个人认为缩表将落在 2022 下半年,最快可能是 6 月份,因为在 3 月加息后,Fed 才会来讨论缩表。
我个人相信 Fed 现在内部早已在讨论缩表,但委员会显然尚未准备好来与市场沟通缩表的前瞻指引。
而缩表这麽大的事情,我个人认为 Fed 需要起次跟市场沟通 2 次,并把缩表规划说得非常清楚之后,才会开始进行,所以比较合理的缩表时间,估计将会落在下半年。
5.最大风险:高通膨
Powell 在记者会上,大概提到了 800 万次的“高通膨压力”,并认为目前美国通膨风险仍在上升阶段,但预计 2022 通膨还是会回落。
Powell 说明,目前美国通膨居高不下,主要仍是供应链所致,白话来说就是供需仍然失衡,且供给侧 (Supply Side) 改善的速度是低于预期。
Powell 强调,目前美国高通膨持续存在,而美国经济要的是长期扩张,所以若要长期扩张,物价势必需要保持稳定。
这边开始进入正题了,我认为这是本次会议的最重要核心,是让我体感上,觉得 Fed 鹰派的地方。我认为 Fed 承认自己落后给菲利浦曲线 (Behind the curve),简单而言,Fed 这次的加息速度大幅落后给通膨。
由于 Fed 在 2021 年对于通膨的误判,先前 Fed 在 2021 年认为通膨在年底就可望自然回落,但也就是因为这件事没有发生,反而通膨还更为严重,所以目前才有使用加息来追赶通膨的压力。但当前宏观环境看,通膨的压力是来自于缺工、供应链紧俏等问题,再加上拜登政府的大力推行财政刺激在那边推波助澜~
所以这一次的通膨是来自于实体经济上的供需失衡问题,并不是金融市场过度投机、企业超额投资等问题,我认为 Fed 在这次的通膨问题上,能做得空间非常有限。
这裡将产生一个不确定性的较大风险,就是 Fed 只能靠货币紧缩去压通膨预期,但实体经济的根本性通膨问题,还是没有获得解决。变成最终 Fed 只能再用更剧烈的紧缩政策,去引导通膨预期走低后,尝试来压低实际通膨率,所以这裡将让 Fed 的紧缩路径,存在著较大不确定性。
比较好的处理方式,应该是直接去解决实体经济上的缺工和供应链/例如我之前提到的塞港问题,让实际通膨率自己走低、而不是靠 Fed 挤压通膨预期之后去引导。
谁可以去把坐在白宫裡疑似患有阿兹海默的白髮老头一巴掌打醒...还我特~
结论:我个人认为 Fed 今年将加息四次,不至于加息五次,而加息四次之预期,相信市场应该已经定价;至于缩表,相信市场尚未定价,估计将落在 2022 下半年,最快可能是 6 月。
如果 Fed 今年加息五次,我会感到非常意外,因为这意味著 Fed 很可能在 2023 年底、2024 年初,就因为美国经济放缓太快而需要降息,Fed 这波操作就会变得非常韭。
最后说说股市的想法目前 Nasdaq 已经插水一段时日,抑制通胀是当务之急,而股市所谓修正才多久已出现V转。对通胀而言意义不大,修正数月才可能有帮助~所以我之前一直描述为“恐慌”。因此对白髮老头而言,怎麽做才有利于中期选举就很清晰了。
最好还是坚持认为市场或已定价加息四次之预期,但缩表预期则是尚未定价的观点。
配置上美股我倾向持有科技权值股,一些 Megacap 的估值我认为合理、前景确定性较高,而这样也可以让你的收益贴著 QQQ 走。
考虑到一堆成长股腰斩,我也愿意加仓接刀成长股,但建议佔据投资组合的比例,或许不要超过 15%,如果选股功力不错,这裡就会开始让你的收益拉开与 QQQ 之类的差距。
最后,我相信人人都会想在市场下跌的环境裡接刀,接刀不是不行,但若接刀失败,斩缆我建议速度要快,我个人不考虑价投的话一次斩缆的比例都是 50% 以上。