对于像计数器这样简单的东西,如果多个线程将增加数量。我读到互斥锁会降低效率,因为线程必须等待。所以,对我来说,原子计数器是最有效的,但我在内部读到它基本上是一个锁?所以我想我很困惑如何比另一个更有效。
7 回答
原子操作利用处理器支持(比较和交换指令)并且根本不使用锁,而锁更多地依赖于操作系统并且在例如 Win 和 Linux 上执行不同。
锁实际上暂停线程执行,为其他任务释放 CPU 资源,但在停止/重新启动线程时会产生明显的上下文切换开销。相反,尝试原子操作的线程不会等待并一直尝试直到成功(所谓的忙等待),因此它们不会产生上下文切换开销,但也不会释放 cpu 资源。
总而言之,如果线程之间的争用足够低,通常原子操作会更快。您绝对应该进行基准测试,因为没有其他可靠的方法可以知道上下文切换和忙等待之间的最低开销是什么。
如果您有一个支持原子操作的计数器,它将比互斥锁更有效。
从技术上讲,原子将锁定大多数平台上的内存总线。但是,有两个改善细节:
- 在内存总线锁期间不可能挂起线程,但在互斥锁期间可以挂起线程。这就是让您获得无锁保证的原因(它并没有说明不锁定 - 它只是保证至少一个线程取得进展)。
- 互斥锁最终会用原子实现。由于您至少需要一个原子操作来锁定互斥锁,并且需要一个原子操作来解锁互斥锁,因此即使在最好的情况下,执行互斥锁至少需要两倍的时间。
最小(符合标准)互斥锁实现需要 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);
}
};
由于其简单性(它不能暂停执行线程),在低争用情况下,此实现很可能优于std::mutex
. 但即便如此,很容易看出,受此互斥锁保护的每个整数增量都需要以下操作:
atomic
释放互斥锁的商店- 比较和交换
atomic
(读取-修改-写入)以获取互斥锁(可能多次) - 整数增量
如果您将其与std::atomic<int>
使用单个(无条件)读取-修改-写入(例如fetch_add
)递增的独立进行比较,则可以合理地预期原子操作(使用相同的排序模型)将优于互斥锁的情况用过的。
原子整数是一个用户模式对象,因为它比在内核模式下运行的互斥锁更有效。原子整数的范围是单个应用程序,而互斥体的范围是机器上所有正在运行的软件。
Java 中的原子变量类能够利用处理器提供的比较和交换指令。
以下是差异的详细描述:http: //www.ibm.com/developerworks/library/j-jtp11234/
大多数处理器都支持原子读取或写入,并且通常支持原子 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
所以第二个版本有一个循环[以防另一个处理器在我们的原子操作之间增加值,因此值不再匹配,并且增量将是错误的]可以变长[如果有很多竞争对手],但通常应该比互斥体版本,但互斥体版本可能允许该处理器进行任务切换。
Mutex
是内核级语义,即使在Process level
. 请注意,它有助于跨进程边界扩展互斥,而不仅仅是在进程内(对于线程)。它更昂贵。
例如,原子计数器AtomicInteger
是基于 CAS 的,通常会尝试尝试操作直到成功。基本上,在这种情况下,线程竞争或竞争以原子方式递增/递减值。在这里,您可能会看到尝试对当前值进行操作的线程正在使用良好的 CPU 周期。
由于您要维护计数器,因此 AtomicInteger\AtomicLong 将是您的用例的最佳选择。