了解哪种更好的唯一方法是对每种情况进行性能分析。结果将因使用情况而异,不同 CPU、不同架构和年份也会有巨大差别。今天最好的可能明天就不是。因此,始终测量并保持测量。
如果没有争用,你所做的事情应该无关紧要。无冲突情况下应始终很快。如果不是这样,则需要调整到无争议情况的不同实现。一个实现可能比另一个实现使用更少或更快的机器指令,但差异应该很小。测试,但我预计结果几乎相同。
接下来看看具有(高)争用的情况:
同样可能需要调整为针对争议情况的实现。一个锁定机制与其他锁定机制不同,类似于无锁方法。
线程<=核心
可以合理地假设所有线程都将运行并执行工作。可能会有短暂的暂停,其中线程被中断,但这确实是例外。这当然只适用于只有一个应用程序执行此操作的情况。所有 CPU 密集型应用程序的线程都加起来在这种情况下。
现在,带锁定的线程将获取锁定并开始工作。其他线程可以等待锁定变为可用并在锁定空闲时立即进行反应。它们可以忙等待或睡眠较长的时间,没有太大关系。锁定限制您只能使用 1 个线程进行工作,并且在切换锁定时几乎不浪费任何 CPU 周期。
另一方面,无锁数据结构都会落入某些尝试和重复循环中。它们将执行工作,并在某些关键点上尝试提交该工作,如果存在争用,则会失败并再次尝试。通常会重复许多昂贵的操作。争用越多,浪费的工作就越多。更糟糕的是,所有对缓存和内存的访问都会减慢最终成功提交工作的线程。因此,您不仅无法更快地前进,而且还会拖慢进度。
对于需要比锁定指令更多 CPU 周期的任何工作负载,我始终会选择锁定方法而非无锁定方法。那里确实不需要太多工作,只留下了锁定自己使用这样一个指令的微不足道情况。内置的原子类型就是这种情况,通常 CPU 具有在硬件中以单个指令无锁定方式执行这些原子操作的操作码,速度相对较快。最终锁将使用这样的指令本身,永远无法比如此微不足道的情况更快。
线程>>核心
如果线程数远远超过核心数,则任何时候只有一小部分线程可以运行。休眠的线程很可能会保持锁定。所有需要锁定的其他线程都必须进入休眠状态,直到保持锁定的线程再次唤醒它们。这可能是锁定数据结构的最坏情况。没有人能做任何工作。
现在有锁的实现(借助操作系统的帮助),其中一个线程试图获取锁会使持有锁的线程接管,直到它释放锁。在这种系统中,浪费仅减少到线程之间的上下文切换。
锁还存在一个问题叫做“雷鸣般的兽群”问题。如果有100个线程在等待一个锁并且该锁被释放,那么根据您的锁实现和操作系统支持,100个线程将会被唤醒。其中一个线程将获得锁,而99个线程则会浪费时间尝试获取锁、失败并回去休眠。你真的不想要一种实现锁受到“雷鸣般的兽群”问题的困扰。
无锁数据结构在这里开始发挥作用。如果一个线程取消调度了,则另一个线程将继续他们的工作并成功地提交结果。该线程在某个时候会再次唤醒并无法提交它的工作并重试。浪费在于被取消调度的线程所做的工作。
3.核心数 < 线程数 < 2 * 核心数
当线程数接近核心数时,存在一个灰色区域。阻塞线程正在运行的机会仍然很高。但这是一个非常混乱的区域。哪种方法更好的结果是相当随机的。我的结论是:如果你没有大量的线程,那么就尽力保持 <= 核心数。
还有一些想法:
有时候,一旦开始工作,需要按特定顺序完成工作。如果一个线程被取消调度,您不能跳过它。您会在某些数据结构中看到这种情况,其中代码将检测到冲突,一个线程实际上完成了不同线程开始的工作,而它自己无法提交结果。现在,如果另一个线程被取消调度,这真的很好。但如果它正在运行,做两次工作就是浪费的。因此,具有此方案的数据结构真正面向方案2。
随着今天进行的移动计算越来越多,考虑您的代码的功率使用变得越来越重要。有许多方法可以优化代码来改变功率使用。但真正使代码使用更少功率的唯一方法是休眠。你会听到更多的是“竞赛休眠”。如果您可以使代码运行得更快,以便它可以更早地睡眠,那么您可以节省电力。但重点在于“更早地睡眠”,或者我应该说“更多地睡眠”。如果有2个线程运行了75%的时间,它们可能会在75秒内解决您的问题。但是,如果您可以通过交替锁定来使用2个线程每次运行50%的时间来解决相同的问题,那么它们需要100秒。但第一个也使用了150%的cpu功率。时间较短,确实如此,但75 * 150%= 112.5 > 100 * 100%。从电源角度来看,较慢的解决方案胜出。锁允许您在无锁状态下交换速度和功率。请记住这一点,并平衡您对速度的需求与充电手机或笔记本电脑的需求。