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


76

对于简单的计数器(例如计数器),如果有多个线程将增加数量。我读到互斥锁会降低效率,因为线程必须等待。因此,对我来说,原子计数器将是最有效的,但是我从内部了解到它基本上是锁吗?所以我想我很困惑,哪一个比另一个更有效。


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

Answers:


53

原子操作充分利用了处理器的支持(比较和交换指令),根本不使用锁,而锁更多地取决于操作系统,并且在Win和Linux等操作系统上的执行情况有所不同。

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

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


46

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

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

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

重要的是要了解它取决于编译器或解释器对平台的支持程度,以为平台生成最佳的机器指令(在这种情况下为无锁指令)。我认为这就是@Cort Ammon所说的“支持”。同样,某些互斥锁可能会保证某些或所有不是由简单原子指令生成的线程的前进进度或公平性。
snow_abstraction

18

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

  • 一种原子传递线程之间状态变化的方法(“锁定”状态)
  • 强制执行由互斥锁保护的内存操作的内存屏障,使其停留在保护区域内。

由于C ++标准要求“同步”关系,因此没有比这更简单的方法了。

最小(正确)的实现可能看起来像这样:

class mutex {
    std::atomic<bool> flag{false};

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);
    }
};

由于其简单性(它无法挂起执行线程),因此在低竞争情况下,此实现可能胜过a std::mutex。但是即使那样,仍然很容易看到受此互斥锁保护的每个整数增量都需要执行以下操作:

  • 一个 atomic发行互斥量商店
  • 一个 atomic比较并交换(读-修改-写)来获取该互斥(可能多次)
  • 整数增量

如果将其与以std::atomic<int>单个(无条件)读-修改-写(例如fetch_add)递增的独立变量进行比较,则可以合理预期,原子操作(使用相同的排序模型)将优于互斥量为用过的。


8

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


1
这几乎是真的。像Linux的Futex这样的现代互斥体实现确实倾向于利用原子操作来避免在快速路径上切换到内核模式。仅当原子操作无法完成所需任务时(例如线程需要阻塞的情况),此类互斥锁才必须跳入内核模式。
Cort Ammon

我认为原子整数的范围是单个进程,这对于应用程序可以由多个进程组成(例如,用于并行性的Python多处理)非常重要。
weberc2


2

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

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

如果您尝试递增,那么您的现代处理器可能支持原子递增/递减,这将非常有用。

如果没有,则可以使用处理器原子cmp&swap或使用互斥锁来实现。

互斥体:

get the lock
read
increment
write
release the lock

原子cmp和交换:

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

因此,第二个版本有一个循环(如果另一个处理器在我们的原子操作之间增加值,因此值不再匹配,并且增加将是错误的),可能会变长(如果有很多竞争者),但通常应该比互斥锁版本,但互斥锁版本可能允许该处理器切换任务。


1

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

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

由于您要维护计数器,因此AtomicInteger \ AtomicLong将是最适合您的用例的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.