《《程序员的自我修养》中关于加锁不能保证线程安全的一个错误》有24个想法

  1. 我觉得您这里举的例子“内存可见性”不是很恰当。因为其实CPU cache有一致性的,你说的那种情况cache protocol就已经处理好了,所以只要一个thread更新了某个内存地址,虽然没有写回内存,另一个thread也不会读到old的数据。内存可见性的问题主要是CPU乱序执行造成的,memory fence主要也是为了控制乱序的scope。如果有其他手段能保证执行顺序,也可以不用锁(lock prefix or atomic instruction)。不知道我这种理解有没有问题。

    1. 按照我的理解,cache coherence只能保证thread 2“最终”一定能看见x的新值,但是具体什么时候能看见cache coherence并不能保证。《Computer Architecture A Quantitative Approach》第四版英文版P207页写的很清楚:“The issue of exactly when a written value must be seen by a reader is defined by a memory consistency model”,而memory consistency与cache conherence是两个问题。Memory barrier主要有两个作用:to complete all pending operations scheduled before the memory barrier(包括把write buffer中的数据更新到内存中去),以及to not re-order past the barrier。其实write bufffer也是造成reordering的可能原因之一,所以我觉得我的理解没有错:)

  2. 请问如果书中代码的lock()其实分别是两个锁,那的确有可能出现书中所说的执行顺序吧?

      1. 也不尽如此,如果锁本身有保证可见性作用的话,那么这里其实是等效于加了volatile关键字。例如Java中的synchronized对象锁本身就可以保证锁住对象的修改对其它线程的可见性。

  3. 谨慎的提下意见,感觉是博主理解错了,原文写的清楚,是由于寄存器优化导致的。就原文的例子来说,假设thread 1执行的是如下代码:
    x=0
    lock()
    x++;
    unlock;
    y = x;
    因为unlock之后还需要用到x的值,而且一些落后或者激进的编译器就会产生下面这样的汇编码:
    mov $0, x //x=0
    lock()
    mov x, %eax //将x值从内存读到eax
    add $1, %eax //eax++,结果并不写回内存
    unlock
    mov %eax, y //y = eax, 可以节省一次读内存操作
    mov %eax, x //这里才将x的值写回内存
    可以看出,thread 1在释放了锁之后才将x的值写回内存。这样就会出现原文中的执行序。
    这种错不是一种臆想的错误,这在早期的编译器,尤其是多线程刚出来的时候特别常见,直到现在一些编译器的高级优化选项还会犯类似(要复杂的多)的错误。

    1. 编译器确实可能做出违法多线程语义的优化。但是,unlock()操作本身是带有memory barrier的,这样意味着x的最新值肯定会被其他线程看得见,即thread 2肯定能在thread 1的unlock操作之后看到x的最新值,这就是我的依据。

      原文的执行顺序本身是不可能发生的,因为thread 2一定要等到thread 1 unlock()执行完之后才能获取锁,进而读取x的值啊。

      1. 关键很多编译器早先根本就没考虑多线程的问题,所以也不存在保证语义的说法。
        unlock的操作可以有很多中实现,比如我现在在做的系统里就有自己实现的spin_lock,没有考虑memory barrier的情况。。

        1. 恩,我明白你的意思。我文中特意拿Pthreads举例,是因为它的标准规定了unlock操作具有可见性的语义。所以Linux上的NPTL肯定能保证pthread_mutex_unlock()的可见性。至于编译器是否会把mov %eax, x 放到unlock之后执行我确实不太确定,可能确实有些编译选项会做这样的优化。但是,如果真的如此的话,那岂不是说使用lock来保证x++这样的操作的原子性都不能得到保证了么?

          1. 我2010年读到这一部分时也有点疑惑,因为如果这段代码都不能保证成功的话,那岂不是加锁根本就没有意义,那么现在世界上跑的大部分程序都会有问题?我没具体研究过编译器代码,但是感觉编译器优化也应该至少保证程序正确才对。否则没人敢用。

  4. 同意之前sponge的意见;问题的关键在于 pthread_mutex_lock/unlock的使用能否阻止编译器对X++进行寄存器优化;如果不能,那么编译器完全可能在unlock之后才把寄存器中当前X的值写入内存,甚至不写也有可能。

    1. 我觉得关键是内存模型,即x处在存储体系的那一层,如果是在CPU cache或者memory中,那么很安全,CPU硬件和pthread会保证一致性。如果在CPU寄存器中,那么显然书中说的问题就会出现。
      作为一个开发多线程的程序员,应该有内存模型的概念,文中的x变量,是否在声明的时候需要加上volatile呢?如果没有加上,那么我感觉书中的代码本身就是有问题的。编译器如果对多线程支持那么烂,那么早期的并发系统都不可能实现出来。

      1. 我觉的原文说的没错哦:“程序的执行可能会呈现如下的执行情况”,具体在啥时候可能呢?当然是在unlock不带路障时可能啦!原文并没有说一定使用了pthread。
        博主讲的内存可见性也没错哦,补充原文介绍另一种执行情况!

  5. 我觉得在一本很有市场的很底层的技术书籍中,出现如此严重的错误,实在不应该。很容易误导我这样的初学者。

  6. 我在做一些多核处理器的编程工作,不基于操作系统的。这款cpu采用了弱一致性模型。我想知道实际开发中c编码该如何组织。

  7. 确实是个错误啊,作者忽略了memory visibility,认为编译器在unlock之后仍然将值保存在寄存器中

  8. 没有错误,人家说的没错。
    x++;这行对应的汇编代码很可能是两行:从内存中加载x到寄存器、对这个寄存器进行+1操作,由于后面的代码很可能还会用到变量x,所以编译器在unlock();之前很可能并没有把x的值写回内存。这个时候如果线程1的时间片用完,调度了线程2,线程2执行同样的操作,最后两个线程写回x的值都将是1。

电子邮件地址不会被公开。 必填项已用*标注