哪个更有效,基本互斥锁或原子整数?

Posted

技术标签:

【中文标题】哪个更有效,基本互斥锁或原子整数?【英文标题】:Which is more efficient, basic mutex lock or atomic integer? 【发布时间】:2013-02-09 23:06:57 【问题描述】:

对于像计数器这样简单的东西,如果多个线程将增加数量。我读到互斥锁会降低效率,因为线程必须等待。所以,对我来说,原子计数器是最有效的,但我在内部读到它基本上是一个锁?所以我想我很困惑如何比另一个更有效。

【问题讨论】:

这个答案是否应该适用于支持 pthread 或某个子集的所有平台和编程语言?我并不完全理解 pthread、操作系统和编程语言之间的关系,但似乎这些关系可能是相关的。 【参考方案1】:

原子整数是一个用户模式对象,因为它比在内核模式下运行的互斥锁更有效。原子整数的作用域是单个应用程序,而互斥体的作用域是机器上所有正在运行的软件。

【讨论】:

这几乎是真的。现代互斥锁实现,如 Linux 的 Futex,确实倾向于利用原子操作来避免在快速路径上切换到内核模式。只有在原子操作未能完成所需任务(例如线程需要阻塞的情况)时,此类互斥锁才需要跳转到内核模式。 我认为原子整数的范围是单个进程,这很重要,因为应用程序可以由多个进程组成(例如,用于并行性的 Python 多处理)。 【参考方案2】:

最小(符合标准)互斥锁实现需要 2 个基本要素:

一种在线程之间以原子方式传递状态变化的方法(“锁定”状态) 内存屏障强制受互斥体保护的内存操作留在保护区内。

由于 C++ 标准要求的“同步-与”关系,没有什么比这更简单的了。

最小(正确)实现可能如下所示:

class mutex 
    std::atomic<bool> flagfalse;

public:
    void lock()
    
        while (flag.exchange(true, std::memory_order_relaxed));
        std::atomic_thread_fence(std::memory_order_acquire);
    

    void unlock()
    
        std::atomic_thread_fence(std::memory_order_release);
        flag.store(false, std::memory_order_relaxed);
    
;

由于其简单性(它不能暂停执行线程),在低争用情况下,此实现很可能优于std::mutex。 但即便如此,很容易看出,受此互斥体保护的每个整数增量都需要以下操作:

atomic 存储释放互斥锁 atomic 比较和交换(读取-修改-写入)以获取互斥锁(可能多次) 整数增量

如果您将其与一个独立的std::atomic&lt;int&gt; 进行比较,该std::atomic&lt;int&gt; 以单个(无条件)读取-修改-写入(例如fetch_add)递增, 可以合理地预期原子操作(使用相同的排序模型)将优于使用互斥锁的情况。

【讨论】:

【参考方案3】:

大多数处理器都支持原子读取或写入,并且通常支持原子 cmp&swap。这意味着处理器本身在单个操作中写入或读取最新值,并且与正常的整数访问相比可能会丢失几个周期,特别是因为编译器无法像正常一样优化原子操作。

另一方面,互斥体是进入和离开的多行代码,在执行期间,访问同一位置的其他处理器完全停止,因此显然对它们有很大的开销。在未优化的高级代码中,互斥锁进入/退出和原子将是函数调用,但对于互斥锁,当您的互斥锁进入函数返回和您的退出函数启动时,任何竞争处理器都将被锁定。对于原子,只有实际操作的持续时间被锁定。优化应该会降低成本,但不是全部。

如果您尝试递增,那么您的现代处理器可能支持原子递增/递减,这会很棒。

如果没有,那么要么使用处理器原子 cmp&swap 实现,要么使用互斥体。

互斥体:

get the lock
read
increment
write
release the lock

原子 cmp&swap:

atomic read the value
calc the increment
do
   atomic cmpswap value, increment
   recalc the increment
while the cmp&swap did not see the expected value

所以第二个版本有一个循环[以防另一个处理器在我们的原子操作之间增加值,因此值不再匹配,并且增量将是错误的]可以变长[如果有很多竞争对手],但通常应该仍然比互斥体版本更快,但互斥体版本可能允许该处理器进行任务切换。

【讨论】:

【参考方案4】:

Mutex 是内核级别的语义,即使在Process level 也提供互斥。请注意,它有助于跨进程边界扩展互斥,而不仅仅是在进程内(对于线程)。比较贵。

原子计数器,例如AtomicInteger,是基于 CAS 的,通常会尝试操作直到成功。基本上,在这种情况下,线程竞争或竞争以原子方式递增/递减值。在这里,您可能会看到尝试对当前值进行操作的线程正在使用良好的 CPU 周期。

由于您想维护计数器,AtomicInteger\AtomicLong 将是您的用例的最佳选择。

【讨论】:

【参考方案5】:

如果你有一个支持原子操作的计数器,它会比互斥锁更有效。

从技术上讲,原子会在大多数平台上锁定内存总线。但是,有两个改善细节:

在内存总线锁定期间无法挂起线程,但可以在互斥锁期间挂起线程。这就是让您获得无锁保证的原因(它并没有说明不锁定 - 它只是保证至少有一个线程取得进展)。 互斥锁最终以原子方式实现。由于您至少需要一个原子操作来锁定互斥体,并且需要一个原子操作来解锁互斥体,因此即使在最好的情况下,执行互斥体锁定至少需要两倍的时间。

【讨论】:

重要的是要理解它取决于编译器或解释器对平台的支持程度,以便为平台生成最佳机器指令(在这种情况下为无锁指令)。我认为这就是@Cort Ammon 所说的“支持”。此外,一些互斥锁可能会保证某些或所有线程的前向进度或公平性,这些线程不是由简单的原子指令产生的。【参考方案6】:

原子操作利用处理器支持(比较和交换指令)并且根本不使用锁,而锁更多地依赖于操作系统并且在例如 Win 和 Linux 上执行不同。

锁实际上会暂停线程执行,为其他任务释放 CPU 资源,但在停止/重新启动线程时会产生明显的上下文切换开销。 相反,尝试原子操作的线程不会等待并一直尝试直到成功(所谓的忙等待),因此它们不会产生上下文切换开销,但也不会释放 cpu 资源。

总而言之,如果线程之间的争用足够低,原子操作通常会更快。您绝对应该进行基准测试,因为没有其他可靠的方法可以知道上下文切换和忙等待之间的最低开销是多少。

【讨论】:

我一直在谷歌搜索和阅读我的教科书好几个小时试图找到这个答案。被高度低估的答案 “锁实际上挂起线程执行”这在一般意义上是不正确的。您可以使用自旋锁或非自旋锁。这完全取决于锁的实现方式,作为一名程序员,了解自己使用的是哪种锁至关重要。【参考方案7】:

Java 中的原子变量类能够利用处理器提供的比较和交换指令。

这里是差异的详细描述:http://www.ibm.com/developerworks/library/j-jtp11234/

【讨论】:

以上是关于哪个更有效,基本互斥锁或原子整数?的主要内容,如果未能解决你的问题,请参考以下文章

在优先级反转问题上,我们应该更改互斥锁或线程的属性吗?

c++ - 互斥锁或群 fcntl.h 锁定只写操作

自旋锁,互斥锁,原子变量性能对比

使用原子和互斥锁 c++ 在类内部进行线程化

互斥锁,自旋锁,原子操作原理和实现

C++多线程1.2-线程安全的保证——互斥量mutex(锁)和原子变量atomic