luyuhuang / luyuhuang.github.io

My blog
https://luyuhuang.tech
19 stars 3 forks source link

谈谈 C++ 中的内存顺序 (Memory Order) - Luyu Huang's Tech Blog #69

Open luyuhuang opened 2 years ago

luyuhuang commented 2 years ago

https://luyuhuang.tech/2022/06/25/cpp-memory-order.html

C++11 将多线程纳入了标准. 一旦涉及到多线程, 就需要考虑并发, 数据竞争 (date race), 线程同步等问题, 为此 C++ 提供了互斥锁 std::mutex, 原子变量 std::atomic 等标准库. 对于原子变量的操作, 有一个很重要的概念就是内存顺序 (memory order), 其中...

wxhnewStar commented 2 years ago

大佬讲得很好,用例也很能体现讲解的知识点~

luyuhuang commented 2 years ago

大佬讲得很好,用例也很能体现讲解的知识点~

谢谢! 不是大佬 😂

edgesider commented 1 year ago

“总结”一节里的第一项是不是应该是“relaxed”?

luyuhuang commented 1 year ago

@edgesider “总结”一节里的第一项是不是应该是“relaxed”?

确实. 感谢指正!

Mer1997 commented 1 year ago

注意 (3) 处的 compare_exchange_strong 的内存顺序是 memory_order_relaxed, 所以 (2) 与 (3) 并不构成 synchronizes-with 的关系. 也就是说, 当循环 (3) 退出时, 并不能保证 thread2 能读到 data.at(0) 为 42.

3.4 节中这里的说法是错误的吧,实际上 thread2 在 RMW 操作后读 data 是安全的. 参见 What does "release sequence" mean?

luyuhuang commented 1 year ago

@Mer1997 3.4 节中这里的说法是错误的吧,实际上 thread2 在 RMW 操作后读 data 是安全的. 参见 What does "release sequence" mean?

不安全吧,因为那个 RMW 操作是 relaxed。这个回答也说了

// Thread 1:
A;
x.store(2, memory_order_release);

// Thread 2:
B;
int n = x.fetch_add(1, memory_order_relaxed);
C;

// Thread 3:
int m = x.load(memory_order_acquire);
D;
  • m = 0, n = 2. Even though the fetch_add operation read the value written by the store, since the fetch_add has a relaxed memory ordering there is no synchronizes-with relationship between the two instruction. We can't say that A happens-before C
Joenhle commented 1 year ago

大佬讲的很棒!

nyanpasu64 commented 1 year ago

Do you know that sobyte.net has translated your article into English, published it at https://www.sobyte.net/post/2022-06/cpp-memory-order/ next to many other articles taken from Chinese websites and translated, and put their own ads on the article? Was this done without your permission?

luyuhuang commented 1 year ago

Do you know that sobyte.net has translated your article into English, published it at https://www.sobyte.net/post/2022-06/cpp-memory-order/ next to many other articles taken from Chinese websites and translated, and put their own ads on the article? Was this done without your permission?

Yes I knew that, they didn't get my permission. I don't know if I have a way to stop them from doing that.

331368068 commented 1 year ago

写得挺好,不过C++11实现多线程条件下的单例模式使用静态局部变量只初始化一次且线程安全的语法特性就行了,不用加锁

class Singleton
{
public:
    static Singleton& getInstance()
    {
        static Singleton value;
        return value;
    }
private:
    Singleton() = default;
    Singleton(const Singleton &rhs) = delete;
    Singleton &operator=(const Singleton &rhs) = delete;
};
luyuhuang commented 1 year ago

写得挺好,不过C++11实现多线程条件下的单例模式使用静态局部变量只初始化一次且线程安全的语法特性就行了,不用加锁

是的,感谢补充!

RyanDDDDDD commented 7 months ago

大佬写的很好!!!

luyuhuang commented 7 months ago

@RyanDDDDDD 大佬写的很好!!!

谢谢!还不是大佬。祝你新年快乐!

davidyangss commented 3 months ago

Hi 打扰了,我有个地方不明白。还请大牛指教下。谢谢啦。 void thread1() { x.store(true, std::memory_order_release); // (1) }

void thread2() { y.store(true, std::memory_order_release); // (2) }

void read_x_then_y() { while (!x.load(std::memory_order_acquire)); // (3) if (y.load(std::memory_order_acquire)) ++z; // (4) }

void read_y_then_x() { while (!y.load(std::memory_order_acquire)); // (5) if (x.load(std::memory_order_acquire)) ++z; // (6) }

则最终不能保证 z 不为 0. 在同一次运行中, read_x_then_y 有可能看到先 (1) 后 (2), 而 read_y_then_x 有可能看到先 (2) 后 (1). 这样有可能 (4) 和 (6) 的 load 的结果都为 false, 导致最后 z 仍然为 0.


要么 先 (1) 后 (2),要么 先 (2) 后 (1);但无论哪种 read_x_then_y 与 read_y_then_x,都会有一个能执行++z的吧? 实在没想出来,不可能的情况。始终不理解,同一个进程不同线程 怎么会观察到 “ 先 (1) 后 (2),先 (2) 后 (1) 同时存在的呢”

davidyangss commented 3 months ago

还有这个: class spinlock { std::atomic flag{false}; public: void lock() { while (flag.exchange(true, std::memory_order_acquire)); // (1) } void unlock() { flag.store(false, std::memory_order_release); // (2) } };


我觉得,memory_order_acquire会将lock前的操作,重排到lock后;相当于扩大了锁的范围。是不是改为 memory_order_acq_rel 更恰当些。


最近在学习Rust,看到了这部分。之前的java经验,对这部分很陌生。看了两三遍,仍然不能够确定自己的理解。还请大佬给确认下。谢谢

luyuhuang commented 3 months ago

@davidyangss 要么 先 (1) 后 (2),要么 先 (2) 后 (1);但无论哪种 read_x_then_y 与 read_y_then_x,都会有一个能执行++z的吧? 实在没想出来,不可能的情况。始终不理解,同一个进程不同线程 怎么会观察到 “ 先 (1) 后 (2),先 (2) 后 (1) 同时存在的呢”

从实现的角度来说,如果 thread1thread2 跑在不同的 CPU 上,那么即使两个线程先后写入两个不同的变量,也不能保证其它线程观察到的修改顺序一样。因为不同的 CPU 有各自的缓存,缓存同步的时机和指令执行的时机可能不同。这就会导致不同的 CPU 看到的顺序不同。

luyuhuang commented 3 months ago

@davidyangss 我觉得,memory_order_acquire会将lock前的操作,重排到lock后;相当于扩大了锁的范围。是不是改为 memory_order_acq_rel 更恰当些。

我觉得没事吧。如果编译器觉得那个指令放后面更合适,那大概率它更后面的代码相关性很大,就让它放呗。基本上我们的原则就是使用尽可能宽松的内存顺序,让编译器做更多的优化。当然如果测试验证某个场景下 acq_rel 跑得更快,也是可以作专项优化调整的。

davidyangss commented 3 months ago

@luyuhuang

@davidyangss 我觉得,memory_order_acquire会将lock前的操作,重排到lock后;相当于扩大了锁的范围。是不是改为 memory_order_acq_rel 更恰当些。

我觉得没事吧。如果编译器觉得那个指令放后面更合适,那大概率它更后面的代码相关性很大,就让它放呗。基本上我们的原则就是使用尽可能宽松的内存顺序,让编译器做更多的优化。当然如果测试验证某个场景下 acq_rel 跑得更快,也是可以作专项优化调整的。

多谢回答!

Nativu5 commented 1 month ago

醍醐灌顶!

luyuhuang commented 1 month ago

醍醐灌顶!

老哥过誉了😂

CrazyMountain commented 1 month ago

写的很不错,比知乎那些详细多了,而且很有逻辑~ 不过还是有几个问题想请教一下: 1、3.3 中的第一个代码示例是两个原子变量,那如果其中代码位置(1)的 x 是非原子变量,代码位置(4)访问的时候也能保证是 true 么,核心问题是原子变量的 memory order 能不能限制之前或者之后的非原子变量的更新也能及时 flush 到主存?(我的理解是会的,因为看到下面 3.5 有一个示例中的 data 就是非原子变量,但还是想确认一下)不同的 memory order 之间在这一场景下有没有啥区别? 2、有一个点其他我看到的文章里都没有涉及到的细节,就是 3.4 代码示例中对于变量 y 的 acquire 是针对某一个作用于 y 的 release 的,对于 y 可能有很多个 release,所以一个原子变量的 acquire 只和自己读到的那个相应的 release 之间有 sync-with 关系对吧?所以很多代码示例中的 while (acquire) 其实就是为了确定循环最后一次的 acquire 读到的一定是示例中的 release 成对的是吧?3.4 的示例中假如(4)和(2)成对,那(3)起到的作用是什么呢?(或者没有作用?) 3、两个 happens-before 没有传递性,是不是因为这两个 happens before 涉及到的线程可能不是相同的两个? 4、文章阐述了 memory order 的定义和作用,请问下有什么资料可以了解到具体是通过什么方案或者技术实现的呢? 不胜感激~

wang-xuewen commented 3 days ago

谢谢大佬讲解,获益良多