52

Raymond Chen一直在做大量 关于无锁算法的系列 文章。除了函数的简单案例之外,所有这些的流行模式似乎是它们实现了自己的 locks。当然,没有处理器锁,但在每个 CPU 上反复循环以确保一致性的概念非常类似于自旋锁。作为自旋锁,它们的效率将低于操作系统附带的通用锁,因为它们在等待其他线程时不会产生对它们的量子的控制。因此,每当有人来找我说“但我的算法是无锁的”时,我的一般反应是“所以”? InterlockedXxx

我很好奇——是否有可用的基准测试显示无锁算法比全锁算法更有优势?

4

10 回答 10

31

除了 InterlockedXxx 函数的简单案例之外,所有这些函数的流行模式似乎是它们实现了自己的锁。

这里的答案似乎都没有真正触及“无锁” CAS循环与互斥锁或自旋锁 之间区别的核心。

重要的区别在于无锁算法可以保证自行取得进展——无需其他线程的帮助。使用锁或自旋锁,任何无法获得锁的可怜线程都完全受拥有锁的线程的支配。无法获取锁的可怜线程除了等待(通过繁忙的等待或操作系统辅助的睡眠)之外什么都不做。

使用在 CAS 上循环的无锁算法,无论其他竞争线程在做什么,都可以保证每个线程都能取得进展。从本质上讲,每个线程都在控制自己的命运。是的,它仍然可能需要循环很多次,但是它循环的次数受到竞争线程数的限制。在大多数情况下,它不能无限循环。(在实践中,可能会发生活锁,例如,由于错误共享而不断失败的LL/SC循环) - 但是线程本身可以再次采取措施来处理这个问题 - 它不会受到摆布另一个持有锁的线程。

至于性能,就看情况了。我见过无锁算法的明显例子,即使在高线程争用情况下,它们的锁定算法也完全胜过它们。std::queue在一台运行 Debian 7 的 x86-64 机器上,我比较了 C++ Boost.Lockfree队列(基于 Michael/Scott 算法)和一个普通的老式std::mutex. 在高线程争用下,无锁版本的速度几乎是原来的两倍。

那为什么呢?好吧,无锁算法的性能最终归结为实现细节。算法如何避免ABA?它如何实现安全的内存回收?有很多变体...标记指针、基于 epoch 的回收、RCU/静止状态、危险指针、一般进程范围的垃圾收集等。所有这些策略都对性能有影响,有些还限制了您的应用程序的一般方式可以设计。一般来说,根据我的经验,引用计数方法(或标记指针方法)往往表现不佳。但是替代方案的实现可能要复杂得多,并且需要更多基于线程本地存储或通用垃圾收集的内存回收基础设施。

于 2016-05-07T19:36:24.167 回答
30

通常,无锁算法每个线程的效率较低 - 正如您所提到的,为了实现无锁算法而不是简单的锁,您正在做更多的工作。

然而,面对争用,它们确实倾向于显着提高整个算法的整体吞吐量。 线程切换延迟上下文切换速度很快,在许多线程上会显着降低应用程序的吞吐量。无锁算法有效地实现了它们自己的“锁”,但它们这样做的方式是防止或减少上下文切换的数量,这就是为什么它们倾向于执行锁定对应物的原因。

话虽如此 - 这大部分取决于所讨论的算法(和实现)。例如,我有一些例程,我设法切换到 .NET 4 的新并发集合,而不是使用以前的锁定机制,并且测量到我的总算法速度提高了近 30%。话虽如此,与基本锁相比,您可以发现许多基准测试表明使用其中一些相同的集合会降低性能。与所有性能优化一样 - 你真的不知道,直到你测量.

于 2011-04-15T18:39:39.820 回答
13

无锁不一定更快,但它可以消除死锁或活锁的可能性,因此您可以保证您的程序始终朝着完成的方向前进。使用锁,很难做出这样的保证——很容易错过一些可能导致死锁的执行序列。

过去,这一切都取决于。至少根据我的经验,速度的差异往往更多地取决于实施中部署的技能水平,而不是它是否使用锁。

于 2011-04-15T19:28:16.003 回答
4

在 x64 上的 Windows 下,一个简单的(在 freelist 前面没有组合数组)无锁 freelist 比基于 mutex 的 freelist 快大约一个数量级。

在我的笔记本电脑(Core i5)上,对于单线程、无锁,我每秒获得大约 3100 万次 freelist 操作,而对于互斥体,每秒大约有 230 万次操作。

对于两个线程(在不同的物理内核上),在无锁的情况下,每个线程我可以获得大约 1240 万个 freelist 操作。使用互斥锁,我每秒可以进行大约 80 千次操作

于 2011-04-18T08:31:35.420 回答
2

真正无锁算法的主要优点是即使任务被搁置,它们也很健壮(请注意,无锁是比“不使用锁”(*)更严格的条件)。虽然避免不必要的锁定具有性能优势,但性能最佳的数据结构通常是那些在许多情况下可以操作锁定,但可以使用锁定来最小化抖动的数据结构。

(*)我已经看到了一些“无锁”多生产者队列的尝试,其中一个生产者在错误的时间被阻碍会阻止消费者看到任何新项目,直到它完成它的工作);这种数据结构不应该真正被称为“无锁”。一个被阻塞的生产者不会阻塞其他生产者取得进展,但可能会任意阻塞消费者。

于 2011-05-08T01:11:57.640 回答
1

无锁算法绝对可以比阻塞算法更快。但当然反过来也是如此。假设实现比锁定计数器部分执行得更好,唯一的限制因素是争用。

以两个 Java 类 ConcurrentLinkedQueue 和 LinkedBlockingQueue 为例。在适度的现实世界竞争下,CLQ 的表现要好于 LBQ。在竞争激烈的情况下,使用暂停线程将使 LBQ 性能更好。

我不同意user237815。synchronized 关键字不需要像以前那样多的开销,但相对于无锁算法,与单个 CAS 相比,它确实有大量与之相关的开销。

于 2011-04-15T20:52:12.263 回答
1

int最近在 [JavaOne Russia][1] 上,Oracle 员工(专门研究 Java 性能和基准测试)展示了一些关于使用 CAS(实际上是无锁,高级自旋锁)并行访问简单计数器的每秒操作数的测量结果和经典锁 ( java.util.concurrent.locks.ReentrantLock)。

据此,自旋锁只有在少数线程尝试访问监视器时才具有更好的性能。

于 2011-04-15T22:58:22.883 回答
0

至少在 Java 中,锁定本身可以非常快。synchronized 关键字不会增加很多开销。您只需在循环中调用同步方法即可自行对其进行基准测试。

仅当存在争用时锁定才会变慢,并且被锁定的进程不是即时的。

于 2011-04-15T18:32:35.437 回答
0

无锁还有一个优点就是不休眠。内核中有些地方不允许你睡觉——Windows 内核有很多这样的地方——这痛苦地限制了你使用数据结构的能力。

于 2011-04-18T08:27:24.307 回答
-1

是的,无锁可确保进度,但除非您手动中止线程,这在某些平台上是可能的,或者在关键部分分配并出现内存不足异常,或者任何类似的愚蠢行为,否则您不需要那样做。正确实施的自旋锁几乎总能胜过无锁方法,因为通常你会在第一次或尝试不成功后做更多的工作。如果您保持旋转时间很短并且通过比较交换指令使 cpu 不堪重负和/或在一段时间后不退缩,将线程时间片提供给其他线程(这使计划外的线程有机会唤醒和释放锁),那么无锁代码可以执行得更好. 除此之外,我认为这是不可能的。我不感兴趣,也不喜欢自旋锁不适合的复杂数据类型,但我仍然觉得设计合理的基于锁的算法几乎总是会更好。我可能是错的。

于 2019-02-01T16:11:47.237 回答