From 74bd1783b83bd96cd80591b179284d52f534c84d Mon Sep 17 00:00:00 2001 From: mq-b <3326284481@qq.com> Date: Thu, 8 Aug 2024 17:22:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=85=E5=AD=98=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=9A=84=E5=86=85=E5=AE=B9=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=B8=80=E8=8A=82=E2=80=9C=E5=8F=AF=E8=A7=81=E2=80=9D=E8=BF=99?= =?UTF-8?q?=E4=B8=AA=E8=AF=8D=E8=AF=AD=E7=9A=84=E6=8F=8F=E8=BF=B0=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...37\345\255\220\346\223\215\344\275\234.md" | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git "a/md/05\345\206\205\345\255\230\346\250\241\345\236\213\344\270\216\345\216\237\345\255\220\346\223\215\344\275\234.md" "b/md/05\345\206\205\345\255\230\346\250\241\345\236\213\344\270\216\345\216\237\345\255\220\346\223\215\344\275\234.md" index 2bef22c2..da5171ed 100644 --- "a/md/05\345\206\205\345\255\230\346\250\241\345\236\213\344\270\216\345\216\237\345\255\220\346\223\215\344\275\234.md" +++ "b/md/05\345\206\205\345\255\230\346\250\241\345\236\213\344\270\216\345\216\237\345\255\220\346\223\215\344\275\234.md" @@ -580,7 +580,7 @@ void reader() { 不过事实上 `std::atomic` 的功能相当有限,单看它提供的修改接口(`=`、`store`、`load`、`exchang`)就能明白。如果要操作其保护的共享指针指向的资源还是得 `load()` 获取底层共享指针的副本。此时再进行操作时就得考虑 `std::shared_ptr` 本身在多线程的支持了。 -[^3]: 不用感到奇怪,之所以多个线程通过 shared_ptr 的副本可以调用一切成员函数,甚至包括非 const 的成员函数 `operator=`、`reset`,是因为 ``shared_ptr` 的**控制块是线程安全的**。 +[^3]: 不用感到奇怪,之所以多个线程通过 shared_ptr 的副本可以调用一切成员函数,甚至包括非 const 的成员函数 `operator=`、`reset`,是因为 `shared_ptr` 的**控制块是线程安全的**。 --- @@ -682,3 +682,40 @@ print("end"); // 2 不禁止就是有可能,但是我们无需在乎,**就算真的 CPU 将 end 重排到 start 前面了,也得在可观测行为发生前回溯了**。所以我一直在强调,这些东西,**我们无需在意**。 好了,到此,基本认识也就足够了,以上的示例更多的是泛指,知道其表达的意思就好,这些还是简单直接且符合直觉的。 + +### 可见 + +可见 是多线程并发编程中的一个重要概念,它描述了一个线程中的数据修改对其他线程的可见程度。具体来说,如果线程 A 对变量 x 进行了修改,那么**其他线程 B 是否能够看到线程 A 对 x 的修改**,就涉及到可见的问题。 + +在讨论多线程的内存模型和执行顺序时,虽然经常会提到 CPU 重排、编译器优化、缓存等底层细节,但真正核心的概念是可见,而不是这些底层实现细节。 + +**C++ 标准中的可见**: + +- 如果线程 A 对变量 x 进行了修改,而线程 B 能够读取到线程 A 对 x 的修改,那么我们说线程 B 能看到线程 A 对 x 的修改。也就是说,线程 A 的修改对线程 B 是***可见***的。 + +C++ 标准通过内存序(memory order)来定义如何确保这种*可见*,而不必直接关心底层的 CPU 和编译器的具体行为。内存序提供了操作之间的顺序关系,确保即使存在 CPU 重排、编译器优化或缓存问题,线程也能正确地看到其他线程对共享数据的修改。 + +例如,通过使用合适的内存序(如 memory_order_release 和 memory_order_acquire),可以确保线程 A 的写操作在其他线程 B 中是可见的,从而避免数据竞争问题。 + +总结: + +- *可见* 关注的是线程之间的数据一致性,而不是底层的实现细节。 + +- 使用 C++ 的内存序机制可以确保数据修改的可见,而不必过多关注具体的 CPU 和编译器行为。 +这种描述方式可以帮助更清楚地理解和描述多线程并发编程中如何通过 C++ 标准的内存模型来确保线程之间的数据一致性。 + +--- + +我知道各位肯定有疑问,我们大多数时候写代码都从来没使用过内存序,一般都是互斥量、条件变量等高级设施,这没有可见性的问题吗? + +没有,这些设施自动确保数据的可见性。例如: `std::mutex` 的 `unlock()` 保证: + +- 此操作*同步于*任何后继的取得同一互斥体所有权的锁定操作。 + +也就是 [`unlock()`](https://zh.cppreference.com/w/cpp/thread/mutex/unlock) *同步于* `lock()`。 + +“*同步于*”:操作 A 的完成会确保操作 B 在其之后的执行中,能够看到操作 A 所做的所有修改。 + +也就是说: + +- `std::mutex` 的 `unlock()` 操作*同步于*任何随后的 `lock()` 操作。这意味着,线程在调用 `unlock()` 时,对共享数据的修改会对之后调用 `lock()` 的线程*可见*。