我在某处读到(无法找到页面)锁定免费数据结构对于“某些工作负载”更有效,这似乎暗示有时它们实际上更慢或者在某些情况下它们的增益可能为零。对锁定指令进行大约100次循环命中来执行原子操作听起来要快得多,而不是等待调度程序将进程重新唤醒,所以对于我来说,在什么情况下无锁数据结构并不明显比旧式的互斥体更不可取。如果锁定在99%的时间内可用且进程无需进入休眠状态,那么互斥锁会更快吗?假设有合适的无锁数据结构,是否有一个很好的经验法则可以知道哪条路可用?
答案 0 :(得分:51)
实现无锁数据结构的一种常见方法是对不可变对象进行可变引用,并且有任何想要更改结构的内容获取引用,生成应用了适当更改的对象的新版本,然后CompareExchange引用指向新对象。如果CompareExchange有效,那很好。如果没有,抛弃新对象,重新获取参考,然后重新开始。
如果生成新对象的价格便宜且争用程度足够低以至于CompareExchange通常可以正常工作,那么这可以很好地工作。如果存在相当大的争用,并且如果生成新对象很慢,则N个线程同时尝试更新可能需要N ^ 2时间才能完成。作为一个极端的例子,假设在CPU上运行100个线程,更新需要100ms的CPU时间(仅在时间片上),并且所有100个线程都尝试一次更新对象。在前十秒内,每个线程将根据原始对象生成一个新对象。其中一个线程将成功执行CompareExchange,而其他线程将全部失败。然后在接下来的9.9秒内,99个线程将生成该对象的新版本,之后一个将成功发布其更新,98将失败。最终的结果是,当锁定系统可以在大约10秒内完成时,无锁方法将花费505秒的CPU时间来执行100次更新。
答案 1 :(得分:7)
无锁数据结构将以这种或那种方式使用架构中的原子语义来执行其核心操作。执行此操作时,您可以使用计算机的整个内部排除机制来确保正确的数据排序或隔离。互斥或关键部分也执行此操作,但它只对单个标志执行一次。互斥锁或关键部分缓慢的情况,即锁定获取失败(存在争用)。在这种情况下,操作系统还会调用调度程序暂停线程,直到释放排除对象为止。
因此,无论何时无锁数据结构在每个核心方法使用多个原子操作时,如果屏蔽关键部分的单个锁可以提供相同的语义 AND ,那么这似乎是合乎逻辑的。实际上,对于所讨论的数据结构,事实上,使用操作系统提供的锁定机制比尝试构建自己的锁定机制更有意义。
答案 2 :(得分:5)
我不知道如何让它更慢,但它肯定会让它变得更难。在这两种方法在性能上几乎完全相同的情况下(或者如果它只需要500皮秒而不是100皮秒时无关紧要),那么选择最简单的方法 - 通常是lock
。
极少数情况下,额外的性能是关键;如果是,我怀疑你最好使用已建立的库中的预卷模式实现。让无锁代码正常工作(以及证明它正常工作在所有情况下)通常非常困难。
另请注意,某些环境提供的锁定级别高于操作系统提供的互斥锁;互斥行为,但没有一些开销(例如,.NET中的Monitor
)。
答案 3 :(得分:1)
我想在答案的这一部分补充一点: “当互斥锁或临界区很慢时,就是锁定获取失败(存在争用)。在这种情况下,操作系统还调用调度程序暂停线程,直到排除对象被释放。”
似乎不同的操作系统可能有不同的方法来确定锁定获取失败时该怎么做。我使用HP-UX,例如它有一种更复杂的方法来锁定互斥锁。这是它的描述:
......另一方面,改变背景是一个昂贵的过程。如果等待时间很短,我们宁愿不进行上下文切换。为了平衡这些要求,当我们尝试获取信号量并将其锁定时,我们要做的第一件事就是短暂的等待。调用例程psema_spin_1()以旋转最多50,000个时钟周期以尝试获取锁定。如果我们在50,000次循环后无法获得锁定,那么我们再调用psema_switch_1()放弃处理器并让另一个进程接管。
答案 4 :(得分:1)
请记住,互斥体很可能实现作为无锁数据结构,因为它使用一个或几个原子对象来表示其状态。这是一种错误的二分法。
最好是考虑是否需要允许多个线程等待访问某些操作或阻塞直到发出信号。每个都需要一个等待线程的队列。前者将线程排队等待访问同步区域,而后者将线程排队等待信号。 Java类AbstractQueuedSynchronizer
和AbstractQueuedLongSynchronizer
提供了这样一个队列 - 特别是 CLH Queue - 可以构建互斥体,条件和其他基于队列的基元。< / p>
如果您的要求只是一个线程承担一组独有的工作,而其他线程可以随意继续进行其他工作,而不是等到它们也可以自己做同样的工作,那么使用无锁技术是可能的。是否这样做会使更快的运行时间下降到基准测试,受制于这些同步控件的争用频率和线程数,以及线程是否还有其他工作要独立执行。
答案 5 :(得分:0)
效率取决于指标。锁定或无等待算法在抢占可能引入死锁或影响调度期限的系统中非常重要。在这些情况下,处理不如正确性重要。
OP考虑将锁定作为互斥锁的替代方案。某些算法不需要来访问共享数据结构。在这些情况下,生产者和消费者都可以同时访问相同的数据结构而不考虑另一个。 shared queue的示例允许单个读取器和单个写入器同时作用于共享实例。这满足了设备驱动程序编写消费者进程可以按需访问的数据的常见需求。
可以允许进程之间更复杂的关系(see Herlihy (1991) for an analysis),具有不同级别的硬件支持。他总结说无等待同步代表了传统的基于锁定技术实现并发对象的质量突破。
这意味着还有一个权衡因素,但不是简单地选择互斥锁和自旋锁之间。
经验法则仍然是关注正确性而非表现。通常可以通过在问题上投入资金来实现绩效,而满足要求通常更加困难。