为什么互斥锁与原子操作的不同之处在于前者是操作系统级别,而后者是处理器级别?

时间:2018-06-23 20:39:45

标签: c++ multithreading c++11 mutex atomic

我正在阅读《 行动中的C ++并发性》 ,在第236-237页中,

  

与互斥锁争用的效果通常不同于   由于使用互斥锁自然会在操作系统级别而不是处理器级别对线程进行序列化,因此可以与原子操作进行竞争。如果您准备好运行足够的线程,则操作系统可以安排另一个线程在一个线程等待互斥时运行,而处理器停顿将阻止任何线程在该处理器上运行。

我在stackoverflow中搜索了互斥体原子操作之间的区别,但没有找到任何具体的解释。我一直认为互斥锁也是由原子操作实现的(或两者都是由相同的硬件指令实现的)。那么,为什么使用互斥锁的线程可以被中断而使用原子操作的线程却不能被中断?

问题“ Which is more efficient, basic mutex lock or atomic integer? ”中的第一个答案是:“ 原子将锁定大多数平台上的内存总线...在运行期间无法挂起线程内存总线锁定,但是可以在互斥锁期间挂起线程”。鉴于互斥锁是由原子实现的,所以它们本质上是相同的,对吗?我想我可能会在这里错过一些事情。有人可以帮我吗?

2 个答案:

答案 0 :(得分:3)

原子操作通常是执行“测试并设置”的操作码。基本上,这将测试内存中的值,并且如果该值为零(例如),则将其递增。虽然这种情况一直在发生,但CPU不会允许任何其他内核访问该位置,并且保证“测试设置”能够不间断地完成。因此,当调用该方法时,最终结果是该值增加了,并且您的程序进入了一个特定的分支,或者没有升高,您的程序又离开了一个不同的分支。

并不是所有的CPU都具有以下功能之一-68000系列没有,而PowerPC系列则没有(我认为-值得纠正。我知道这在PowerPC VME系统中是很麻烦的,因为基于68000的上一代机器可以在远程板上进行测试和设置),可以肯定的是,早期的X86也没有。我很确定所有主要的现代CPU都可以使用-这非常有用。

有效地,“测试设置”为您提供了计数信号量,这就是它们的用途。但是,在库中只有一点点修炼,它也可以用作互斥锁(这是一个二进制信号量,只能由接收它的线程来提供)。

如今, AFAIK信号灯和互斥锁通过使用CPU上可用的“测试并设置”操作代码来实现。但是,在没有“测试设置”操作码的平台上,其行为必须由OS进行综合,可能涉及ISR,中断禁止等。最终结果的行为相同,但速度要慢得多。同样在这些平台上,必须使用互斥量来合成“原子”以保护其值。

因此,我怀疑在内核级别讨论互斥锁序列化是指内核已实现互斥锁且CPU支持原子操作的系统。

值得记住的是,即使OS继续使用CPU测试和设置操作代码来实现互斥锁的互斥部分,调用/给互斥锁的调用也涉及内核制定调度决策。不能直接从程序内部调用测试设置操作码;内核甚至不知道它发生了。因此,互斥锁是确保有争用时高优先级线程首先运行的好方法,而测试设置操作代码则可能不是(这是先到先得的原则)。这是因为CPU没有线程优先级的概念,这是OS开发人员梦dream以求的抽象概念。

通过深入了解Boost C ++库的源代码,您可以学到很多有关这种事情的方法。诸如共享指针之类的东西依赖于互斥,Boost可以通过多种不同方式实现互斥。例如,在具有测试和设置样式的操作码上使用它们,或者使用POSIX互斥锁库函数调用,或者如果您告诉您程序中只有1个线程,那么它根本不会打扰。 。

Boost值得在可能的地方使用操作码实现自己的互斥机制;它不需要进程间(仅是线程间)功能,而完整的POSIX互斥体是进程间的,对于Boost的要求而言是过大的选择。

使用Boost,您可以使用一些#defines覆盖默认选择。因此,您可以通过编译单个线程的程序而不会在共享指针中相互排斥,从而加快单线程程序的速度。有时确实是有用的。我不知道的是,既然它们已经吸收了智能指针并使其成为自己的指针,那么在C ++ 11及以后的版本中是否已经丢失了它。

编辑

值得一看的futexes,这是Linux用作互斥量,信号量等的基础。futex的思想是使用原子操作来完全实现大部分功能。用户空间,仅在绝对必要时才诉诸系统调用。结果是,只要没有太多争用,互斥体或信号量之类的高级东西就比过去总是用来进行系统调用的糟糕年代要高效得多。 FUTEXes自2003年左右开始出现在Linux中,因此我们已经受益于他们15年了。基本上没有必要担心互斥锁与原子操作的效率过高-它们在同一件事上相距不远。

可能更重要的是针对易于阅读的干净整洁的源代码,并使用有助于此目的的库调用。例如,以互斥量为基础使用原子操作以牺牲简单的源代码为代价可能不值得。当然,在像VxWorks这样的平台上,它实际上并没有真正拥有内核/用户空间的概念,并且是围绕闪电般快速的上下文切换时间进行设计的,因此可以轻而易举地使用互斥量和信号量来实现简单性。

例如,使用互斥锁控制哪个线程可以访问特定的网络套接字是一种使用内核和线程优先级来管理通过该套接字发送的不同类型消息的优先级的方法。源代码非常简单-线程仅使用套接字提供/提供互斥体,仅此而已。没有队列管理器,没有优先级决策代码,什么也没有。所有这些都是由操作系统调度线程响应互斥量接受/给予而完成的。在VxWorks上,这非常高效,得益于操作系统解决优先级反转的问题,并且开发时间很少。在Linux上,尤其是应用了PREEMPT_RT补丁集并作为实时优先级线程运行的Linux,它也还不错(因为它还可以解决优先级倒置问题,我收集的Linus对此并不太在意)。在没有支持互斥体的FUTEX且上下文切换时间也很昂贵的操作系统(例如Windows)上,效率会很低。

答案 1 :(得分:2)

互斥锁和无锁原子操作之间的根本区别在于,获取互斥锁所需的时间可能比执行无锁原子操作所需的时间长得多,因此拥有代码是不可接受的只需在互斥锁上忙于等待,直到可用为止。取而代之的是,让操作系统找到互斥锁繁忙时处理器可以执行的其他工作,然后请求释放互斥锁以重新启动一直在等待的线程,这将更为有用。

请注意,某些语言不能保证其所有原子操作都是无障碍的,少了无锁的。如果实现通过将原子操作包装在私有互斥体中来处理原子操作,并且执行原子操作的任务被切换出,则其他所有希望执行此操作的任务都可能被阻止,直到原始任务有机会再运行一次。如果这些任务花了所有时间检查互斥对象是否可用,则持有互斥对象的任务可能会暂时无法运行。如果在互斥锁上等待的任务的优先级高于持有互斥锁的任务的优先级,则后者可能永远无法获得执行的机会,并且系统可能永远死机。