作者回复: 1. “可见”,可以理解成获得和释放操作的两个线程能观察到相同的内存修改结果。
2. 原则上任何多线程访问的变量应该要么是原子量,要么有互斥量来保护,这样最安全。特别要考虑内存序的,当然就是有多个有逻辑相关性的共享变量了。对于单个的变量,比如检查线程是否应该退出的布尔变量,只要消除了编译器优化,不需要保证访问顺序也可以正常工作;这样原子量可以使用 relaxed 的访问方式。
作者回复: 用原子量的地方,粗想一下,你用锁都可以。但如果锁导致阻塞的话,性能比起原子量那是会有好几个数量级的差异了。锁即使不导致阻塞,性能也会比原子量低——锁本身的实现就会用到原子量,是个复杂的复合操作。
反过来不成立,用互斥量的地方不能都改用原子量。原子量本身没有阻塞机制,没有保护代码段的功能。
作者回复: 这篇太简单了,基本上只是覆盖尝试加锁这一步(大致是 compare_exchange_strong)。而且,现代操作系统上谁会用关中断啊。
最关键的是,一个线程在加锁失败时会发生什么。操作系统会挂起这个线程,并在锁释放时可能会重新唤起这个线程。文中完全没有提这个。
作者回复: #1
不一定。比如,对于 store,生成可能就只是 mov 指令加个 mfence。
#2
是。
#3
你可以对比一下编译器生成的汇编代码:
https://godbolt.org/z/UHsDRj
作者回复: 看参考资料4吧。如果嫌太长,就只看代码,编译器和处理器眼里允许重排成的样子。
简单说,就是赋值顺序的问题。至少在某些处理器上,其他线程可能先看到 inst_ptr_ 被修改,再看到单件的构造完成。
作者回复: 思考得挺深入,很好。👍
操作系统的上下文切换和内存序的关系我略有不同意见。内存屏障的开销我查下来大概是 100、200 个时钟周期,也就是约 50 纳秒左右吧。而 Linux 的上下文切换开销约在 1 微秒多,也就是两者之前的性能差异超过 20 倍。因此,内存屏障不太可能是上下文切换性能开销的主因。
上下文切换实际需要做的事情非常多,那应该才是主要原因。
作者回复: 你从需求方面理解的 1、2、3 我觉得都对,很好!
“互斥量只有和锁配合”这个提法我觉得很怪:互斥量是个对象,(加/解)锁是互斥量支持的动作——如果你指 lock_guard 之类的类,那是辅助的 RAII 对象,目的只是自动化互斥量上的对应操作而已。
你可能是被“操作系统中锁的实现原理”这样的提法带偏了。没有作为名字的专门锁对象,只有互斥量、条件变量、原子量。我也被带偏了,我在某个评论里说“锁”的时候,指的就是互斥量加锁。
作者回复: memory_order_seq_cst 不是拿来和 memory_order_acq_rel 对比的,而是和 memory_order_relaxed 对比的。正如我在另外一个回答里说的,这里使用 memory_order_acq_rel 可能是非法的。比如 load,只能使用 relaxed、acquire 和 seq_cst,并且后两者是等价的。
作者回复: 好问题。这个问题我之前没细究,但现在仔细一看,常见架构上内存序参数对 fetch_add 是没影响的……似乎读-修改-写操作里,一般都是实现成顺序一致的。
也有例外,如 Power、Raspbian Buster、RISC-V:
https://godbolt.org/z/Du85RX
作者回复: 别漏了前面那几句:
「`memory_order_seq_cst`:顺序一致性语义,对于读操作相当于获取,对于写操作相当于释放」
作者回复: 按标准的规定,store 只能用 relaxed、release 或 seq_cst,load 只能用 relaxed、acquire 或 seq_cst,等等。其他组合在标准中明确说是未定义行为,就算能过也有点凑巧,不保证换个编译器或甚至换个版本还能继续工作。
不要这么做。
作者回复: 计算的世界真是复杂。C++是为了性能,让你能够看到这些复杂性而已。对性能没那么关注的,可以把这些复杂性隐藏掉。
作者回复: delay部分和第二个问题的回答是“是”。
第一个问题你这么说似乎也对,但这个asm语句的主要目的是防止编译器做出任何重排,而没有对处理器提出要求。结果是会跟你说的一样。