锁、内存屏障和信号量的区别

12
这篇文章:http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf(第12页)似乎区分了锁和内存屏障。
我想知道锁、内存屏障和信号量之间的区别是什么?
(虽然其他问题可能提到锁和同步对象之间的区别,但我没有找到关于锁和内存屏障之间的区别的问题。)
3个回答

15

内存屏障(也称为栅栏)是一种硬件操作,它确保对全局可见存储的不同读写的排序。在典型现代处理器上,内存访问是流水线化的,可能会无序发生。内存屏障确保这种情况不会发生。完整的内存屏障将确保在其之前发生的所有加载和存储在其之后发生的任何加载或存储之前发生。(许多处理器都支持部分屏障;例如,在Sparc上, membar#StoreStore 确保所有出现在它之前的存储将在出现在其之后的任何存储之前对所有其他进程可见。)

这就是内存屏障所做的全部。它不会阻止线程,也不会做其他任何事情。

互斥量和信号量是在操作系统中实现的更高级别的基元。请求互斥锁的线程将被阻塞,并且由操作系统挂起其执行,直到该互斥锁可用。操作系统中的内核代码将包含内存屏障指令以实现互斥锁,但它做了更多的工作;内存屏障指令将暂停硬件执行(所有线程),直到满足必要条件为止-最多一微秒的时间,并且整个处理器在此期间停止。当您尝试锁定互斥锁并且另一个线程已经拥有它时,操作系统将挂起您的线程(仅挂起您的线程-处理器继续执行其他线程),直到持有互斥锁的线程释放它,这可能需要几秒钟、几分钟甚至几天。(当然,如果超过几百毫秒,那么可能是一个错误)

最后,在信号量和互斥量之间实际上没有太大的区别;互斥量可以被认为是计数为一的信号量。


1
这里还有人告诉我,我必须使用内存屏障(memory barrier)来防止该变量被缓存。但我不明白在这种情况下内存屏障(确保正确的执行顺序)会有什么帮助。只有使用volatile来明确告诉编译器它的值可以随时改变,所以不要优化它。还有信号量/互斥锁,以便m_NumRunningThreads==0不会触发多次。我的理解正确吗? - xcrypt
1
@xcrypt 缓存更多或少是一个误导,除非您使用缓存的扩展含义。处理器同步缓存;除非您请求,否则它们不会同步读写管道中的数据。实际上,“volatile”不能保证任何东西,因为我们担心硬件在执行时重新排序。当然,您使用的任何技术都将(或应该)防止编译器在其周围移动访问。 - James Kanze
1
虽然volatile不能替代内存屏障或锁,但根据各种帖子和文章,它可以用于使编译器不优化某些部分。这表明变量的值可以随时更改(以及其他一些内容)。 - xcrypt
1
@xcrypt,您何时想停止硬件重新排序呢?(当然,根据Posix的规定,如果您什么也不做,这是未定义的行为。) - James Kanze
1
@xcrypt 它在Posix标准的§4.10中: "应用程序应确保通过多个控制线程(线程或进程)访问任何内存位置受到限制,以便没有控制线程可以在另一个控制线程可能正在修改它时读取或修改内存位置。使用同步线程执行和与其他线程同步内存的函数来限制此类访问。" 在Posix中未指定membar等的使用,但提供了额外的同步内存的方法,因此可以达到相同的效果。 - James Kanze
显示剩余9条评论

14
  • 内存屏障是一种用于排序内存访问的方法。编译器和CPU可以改变操作顺序以进行优化,但在多线程环境下,这可能会导致问题。与其他方式的主要区别在于这种方法不会停止线程。
  • 锁或互斥体(mutex)确保代码只能被一个线程访问。在这个部分中,您可以将环境视为单线程,因此不需要内存屏障。
  • 信号量(semaphore)基本上是一个可以增加(v())或减少(p())的计数器。如果计数器为0,则p()会使线程暂停,直到计数器不再为0。这是同步线程的一种方法,但我更喜欢使用互斥体或条件变量(condition variables) (有争议,但这是我的观点)。当初始计数器为1时,信号量称为二进制信号量,并且类似于锁。

锁和信号量之间的一个重要区别在于线程拥有锁,因此不应尝试解锁其他线程,而对于信号量则不是这种情况。


1
等一下,我被告知互斥锁是某种信号量? - xcrypt
1
如果你已经了解了信号量和互斥锁,那么你可以把互斥锁看作是一种二进制信号量,但它们并不完全相同(也请参阅维基百科链接)。然而,如果你已经了解了互斥锁和条件变量,并且学习了信号量,那么你可以将信号量视为带有计数器和条件的条件变量,在尝试使计数器为负时线程应该等待。 - stefaanv
1
@xcrypt:是的,“mutex”是“mutual exclusion semaphore”的简称。 - Jerry Coffin
1
@JerryCoffin:我不完全同意,即使这是官方名称,也仅仅是因为信号量比较古老的缘故,但我也见过“互斥信号量”用来表示二元信号量。主要区别在于信号量有一个计数器而互斥量没有,因此语义上,互斥量是锁定的,但信号量的行为取决于它们的计数器。(顺便说一下,你是否尝试过使用一个带条件变量的信号量而不是互斥量?我没有,但我猜它不起作用。) - stefaanv
锁和互斥量包含内存屏障,正如James Kanze的回答中所提到的。这也在https://stackoverflow.com/a/28771322/4692662中提到。 - Steve Chavez

2

现在先简单解释一下。

锁定

是一个原子测试,用于判断这段代码是否可以继续执行。

lock (myObject)
{
    // Stuff to do when I acquire the lock
}

通常它是一条单CPU指令,作为一个原子操作来测试和设置变量。更多请参见http://en.wikipedia.org/wiki/Test-and-set#Hardware_implementation_of_test-and-set_2内存栅栏 是提示处理器不能乱序执行这些指令的提示。如果没有它,指令可能会乱序执行,就像在双重检查锁定中,两个空指针检查可能会在锁定之前执行。
Thread.MemoryBarrier();
public static Singleton Instance()
{
    if (_singletonInstance == null)
    {
        lock(myObject)
        {
            if (_singletonInstance == null)
            {
                _singletonInstance = new Singleton();
            }
        }
    }
}

这是一组CPU指令,用于实现内存屏障,明确告诉CPU不能乱序执行操作。

信号量

与锁类似,但通常用于多个线程。例如,如果您可以处理10个并发磁盘读取操作,则会使用信号量。根据处理器的不同,这可能是自己的指令,或者是测试和设置指令,需要在中断周围进行更多的工作(例如,在ARM上)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接