V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Suomea
V2EX  ›  程序员

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

  •  
  •   Suomea · 350 天前 · 1030 次点击
    这是一个创建于 350 天前的主题,其中的信息可能已经有所发展或是发生改变。

    C

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

    个人猜测?

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

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

    Java

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

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

    13 条回复    2023-04-14 17:17:38 +08:00
    LeegoYih
        1
    LeegoYih  
       350 天前
    八股文要从硅原子原理开始背了吗?
    yinmin
        2
    yinmin  
       350 天前
    多线程访问同一个变量,这个变量是存放在同一个地址空间里的,没有同步概念,也没有可视概念。

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

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

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


    《计算机组成原理》 相关书籍
    Suomea
        11
    Suomea  
    OP
       349 天前
    @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# 支持的指令有限。
    dode
        13
    dode  
       349 天前
    你看 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 写入的值。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1587 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 16:57 · PVG 00:57 · LAX 09:57 · JFK 12:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.