无锁算法真的比锁定完全算法表现更好吗?

时间:2011-04-15 18:28:42

标签: multithreading synchronization lock-free

Raymond Chen一直在进行huge series on lockfree algorithms。除了InterlockedXxx函数的简单情况之外,似乎所有这些的主要模式是它们实现自己的锁。当然,没有处理器锁,但是在每个CPU上反复循环以确保一致性的概念非常类似于自旋锁。作为一个自旋锁,它们的效率将低于操作系统附带的一般锁,因为它们在等待其他线程时不会控制其量子。因此,每当有人来找我并说“但我的算法是无锁的”时,我的一般反应是“如此”?

我很好奇 - 是否有基准测试可以显示锁定免费算法比其完全锁定的算法有优势?

10 个答案:

答案 0 :(得分:26)

一般来说,无锁算法每个线程的效率较低 - 正如您所提到的,您正在做更多的工作,以实现无锁算法,而不是简单的锁。

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

话虽如此 - 大部分都取决于所讨论的算法(和实现)。例如,我有一些例程,我设法切换到.NET 4的新并发集合,而不是使用以前的锁定机制,并测量了我的总算法速度超过30%的改进。话虽这么说,但与基本锁相比,您可以找到许多使用这些相同集合的性能降低的基准测试。与所有性能优化一样 - 在度量之前,您真的不知道。

答案 1 :(得分:17)

  

除了InterlockedXxx功能的简单情况之外,它似乎也是如此   所有这些的普遍模式是他们实施他们的   自己的锁。

这里的答案似乎都没有达到“无锁”和“无锁定”之间差异的核心。 CAS循环和互斥锁或自旋锁。

重要的区别在于无锁算法可以保证在自己的上取得进展< - >,而无需其他线程的帮助。使用锁定或旋转锁定,任何不能获得锁定的不良线程完全完全由拥有锁定的线程支配。除了等待(通过忙碌等待或操作系统辅助睡眠)之外,无法获得锁定的糟糕线程无能为力。

使用在CAS上循环的无锁算法,无论其他竞争线程正在做什么,每个线程都可以保证取得进展。每个线程基本上都控制着自己的命运。是的,它仍然可能需要多次循环,但它循环的次数受竞争线程数量的限制。在大多数情况下,它无法无限循环。 (实际上,由于例如由于错误共享而导致失败的LL/SC循环,因此可能发生实时锁定) - 但是线程本身可以采取措施来处理此问题 - 它不受另一个持有锁的线程的支配。

至于表现,取决于。我已经看到了无锁算法的公然例子,即使在高线程争用下,他们的锁定对手也完全没有执行。在运行Debian 7的x86-64机器上,我比较了C ++ Boost.Lockfree队列(基于Michael / Scott算法)和普通旧std::queue环绕std::mutex之间的性能。在高线程争用下,无锁版本几乎是两倍慢。

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

答案 2 :(得分:11)

无锁不一定更快,但它可以消除死锁或活锁的可能性,因此您可以保证您的程序将始终在完成方面取得进展。有了锁,很难做出任何这样的保证 - 很容易错过一些导致死锁的可能执行序列。

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

答案 3 :(得分:5)

在x64上的Windows下,直接(空闲列表前面没有组合数组)无锁空闲列表比基于互斥锁的空闲列表快一个数量级。

在我的笔记本电脑(酷睿i5)上,对于单线程,无锁定,我每秒可获得约3100万个空闲列表操作,而互斥锁每秒约有230万次操作。

对于两个线程(在不同的物理内核上),无锁定,每个线程获得大约1240万个空闲列表操作。使用互斥锁,我每秒可以进行大约80次 THOUSAND 操作。

答案 4 :(得分:1)

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

获取两个Java类ConcurrentLinkedQueue和LinkedBlockingQueue。在温和的现实世界争论中,CLQ的表现优于LBQ。由于争用很多,使用挂起线程可以使LBQ表现更好。

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

答案 5 :(得分:1)

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

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

答案 6 :(得分:0)

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

锁定仅在存在争用时变慢,并且锁定的过程不是即时的。

答案 7 :(得分:0)

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

http://dl.dropbox.com/u/19116634/pics/lock-free-vs-locks.png //抱歉,我无法粘贴图片

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

答案 8 :(得分:0)

无锁也具有不睡觉的优点。内核中有些地方不允许你睡觉 - Windows内核有很多内容 - 这会严重限制你使用数据结构的能力。

答案 9 :(得分:0)

是的,锁定自由可以确保进度,但是除非您手动中止某些平台上可能发生的线程或在临界区中分配内存而导致内存不足异常或类似此类的愚蠢行为,否则不需要。 如果执行效果不佳,则正确实施的自旋锁几乎总会击败无锁方法,因为通常您会第一次或在尝试不成功后进行更多工作。 如果您使用比较交换指令使旋转时间短并且使cpus不堪重负和/或在给线程时间片分配给其他线程一段时间后不退缩(这给了计划外线程唤醒和释放锁的机会),那么无锁代码会更好地执行。除此之外,我认为这是不可能的。我既不感兴趣,也没有兴趣使用自旋锁不适合的复杂数据类型,但我仍然认为,正确设计的基于锁的算法几乎总是会更好。我可能是错的。