多线程中的锁如何保证变量和可视性

2023-04-13 17:14:38 +08:00
 Suomea

C

C 语言 POSIX 线程目定义了互斥量,如果临界区的代码更新了全局变量的值,那么在临界区结束之后通过什么机制来保证全局变量的可视性?

个人猜测?

在线程 unlock 的时候刷新自己缓存的值到主存,这样的话由于互斥访问所有的线程看到的都是最新的数据,并且临界区执行完成后的刷新保证后续的线程看到的也是最新的。

但是这样还有问题,就是线程是刷新线程本地的全部缓存到主存?还是只是和临界区相关变量的缓存到主存?具体的底层指令是啥或者原理是啥?

Java

其实 Java 也有一样的问题,搜索给出的答案都是内存屏障、Happen-before 原则,但是没有看到内存屏障、Happen-before 这些东西的底层原理或者伪代码解释~

希望能给出详细的解释或者权威的引用文档~

1061 次点击
所在节点    程序员
13 条回复
LeegoYih
2023-04-13 17:25:51 +08:00
八股文要从硅原子原理开始背了吗?
yinmin
2023-04-13 17:32:33 +08:00
多线程访问同一个变量,这个变量是存放在同一个地址空间里的,没有同步概念,也没有可视概念。

锁是为了事务的原子性。例如:a=a+1 ,a 原来值是 1 ,如果 2 个线程同时操作,都读到 1 ,然后都是回写 2 ,就出问题了。1 个线程加锁后,另外一个线程就会等到解锁再操作,避免了冲突。
Suomea
2023-04-13 17:33:00 +08:00
@LeegoYih 卷起来!!! 其实不是

最近在看《操作系统导论》并发的部分,里面只讲了 LOCK#、CAS 解决原子性的问题,实现了互斥访问。但是想想没有讲述到可视性问题,遂求问~
Ericcccccccc
2023-04-13 17:35:40 +08:00
原理是搜 MESI, 这个其实是硬件保证的. 不同 cpu 架构还有差别. (比如某些架构下, 天生强一致, 不需要内存屏障也能行
Suomea
2023-04-13 17:35:51 +08:00
@yinmin 那如果多个线程是在多个 CPU 核心上运行呢,如果全局变量没有加 volatile 修饰,那么这个变量会缓存在 CPU 内部的 L1 吗?如果会临界区结束,要刷新 L1 到主存吗?如果要又是什么机制呢?啊啊啊~~~
Suomea
2023-04-13 17:44:20 +08:00
@Ericcccccccc 假设 IA-32 。那么锁是怎么和 MESI 机制结合的呢?是不是进入临界区之后,所有的缓存都使用了 MESI 机制,而不是临界区的缓存就不使用 MESI 机制了吗?
Inn0Vat10n
2023-04-13 18:36:25 +08:00
多核之间 cache 里数据的一致性问题是硬件管的,它有自己的一致性协议,软件这层不用管
dode
2023-04-14 09:14:03 +08:00
最下面就是 CPU ,寄存器,指令集,提供一些原语保证了
Suomea
2023-04-14 14:52:29 +08:00
@dode 是的,什么原语,怎么生效的呢?和锁的联动机制是啥?
dode
2023-04-14 15:24:44 +08:00
硬件对同步的支持-TAS 和 CAS 指令
https://www.cnblogs.com/upnote/p/13193856.html

cpu 硬件同步原语
https://baike.baidu.com/item/CAS/7371138


《计算机组成原理》 相关书籍
Suomea
2023-04-14 16:43:18 +08:00
@dode 这个我知道,我们可以通过 CAS + LOCK# 来实现互斥,即加锁。但是注意这里只是锁,而不是临界区的共享变量。举个自旋锁例子

mutex_t {
int flag; // 初始化等于 0 。1 表示锁被占用
}

lock(mutex_t *mutex) {
while(asm LOCK# CAS(mutex->flag, 0, 1) = 0)
;
}

unlock(mutex_t *mutex) {
mutex->flag = 0;
}

int a;

void 临界区() {
lock();
…… // 对 a 进行操作
unlock();
}

这里 CAS 只是保证了锁的正确性。但是我的问题是临界区的代码并没有对 a 进行额外的(刷新缓存,或者什么的)操作,至少代码上看是这样。那难道临界区的所有语句都加上 LOCK#,不应该,因为 LOCK# 支持的指令有限。
Suomea
2023-04-14 17:13:33 +08:00
dode
2023-04-14 17:17:38 +08:00
你看 java 这个例子

https://www.jianshu.com/p/06717ac8312c
并发编程-( 4 )-JMM 基础(总线锁、缓存锁、MESI 缓存一致性协议、CPU 层面的内存屏障)
3.3.2 、JMM 层面的内存屏障


```java
class VolatileExample {
int a = 0;
volatile boolean flag = false;

public void writer() {
a = 1; //1
flag = true; //2
}

public void reader() {
if (flag) { //3
int i = a; //4
...
}
}
}
```

假设线程 A 执行 writer()方法之后,线程 B 执行 reader()方法,那么线程 B 执行 4 的时候一定能看到线程 A 写入的值吗?注意, [a 不是 volatile 变量] 。
      答案是肯定的。因为根据 happens-before 规则,我们可以得到如下关系:
      根据程序顺序规则,1 happens-before 2 ; 3 happens-before 4 。
      根据 volatile 规则,2 happens-before 3 。
      根据传递性规则,1 happens-before 4 。
      因此,综合运用程序顺序规则、volatile 规则及传递性规则,我们可以得到 1 happens-before 4 ,即线程 B 在执行 4 的时候一定能看到 A 写入的值。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/932290

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX