锁定未锁定的互斥锁的效率如何?互斥锁的成本是多少?


149

在低级语言(C,C ++或任何其他语言)中:我可以选择是否拥有一堆互斥对象(例如pthread给我的东西或本机系统库提供的东西)或一个对象的单个对象。

锁定互斥锁的效率如何?即可能有多少个汇编程序指令,它们需要多少时间(在互斥锁未锁定的情况下)?

互斥锁的成本是多少?确实有很多互斥体是一个问题吗?还是我可以在代码中抛出与变量一样多的互斥量int变量,这并不重要吗?

(我不确定不同的硬件之间有多少区别。如果有,我也想知道它们。但是大多数情况下,我对通用硬件很感兴趣。)

关键是,通过使用许多互斥量(每个互斥量仅覆盖对象的一部分)而不是整个对象的单个互斥量,我可以保护很多块。我想知道我应该走多远。即我应该尽可能真正地保护任何可能的块,无论这意味着多少复杂和多少互斥量?


WebKits博客文章(2016)中有关锁定的问题与该问题非常相关,并解释了自旋锁,自适应锁,futex等之间的区别。


这将是实现和特定于体系结构的。如果具有本机硬件支持,则某些互斥锁几乎不会花费任何费用,而其他互斥锁则会花费很多。没有更多信息就不可能回答。
吉安2010年

2
@吉安:好吧,我当然暗示了这个问题。我想了解常见的硬件,但也要知道例外情况。
艾伯特2010年

我真的在任何地方都看不到这种含义。您询问“汇编程序指令”-答案可能从1条指令到上万条指令不等,具体取决于您所讨论的体系结构。
吉安2010年

15
@吉安:然后请给出准确的答案。请说一下x86和amd64上的实际情况,请给出一个体系结构示例,其中1条指令,而1条指令10k。我不是很想知道我的问题吗?
艾伯特2010年

Answers:


120

我可以选择一堆互斥对象或一个对象。

如果您有许多线程并且对对象的访问经常发生,那么多个锁将增加并行性。以可维护性为代价,因为更多的锁定意味着更多的锁定调试。

锁定互斥锁的效率如何?即有可能有多少个汇编程序指令,它们需要多少时间(在互斥锁未锁定的情况下)?

精确的汇编程序指令是互斥锁的开销最小- 内存/缓存一致性保证是主要的开销。而且不经常使用特定的锁-更好。

互斥锁由两个主要部分组成(过于简化):( 1)指示互斥锁是否已锁定的标志,以及(2)等待队列。

更改标志只是很少的指令,通常无需系统调用即可完成。如果互斥锁被锁定,则syscall会把调用线程添加到等待队列中并开始等待。如果等待队列为空,则解锁很便宜,但否则需要syscall才能唤醒其中一个等待进程。(在某些系统上,廉价/快速syscall用于实现互斥锁,仅在发生争用时,它们才成为慢速(正常)系统调用。)

锁定未锁定的互斥锁确实很便宜。解锁没有竞争的互斥体也很便宜。

互斥锁的成本是多少?确实有很多互斥体是一个问题吗?还是可以在代码中抛出与具有int变量一样多的互斥量变量,这并不重要吗?

您可以根据需要将尽可能多的互斥变量放入代码中。您仅受应用程序可以分配的内存量限制。

摘要。用户空间锁(尤其是互斥锁)很便宜,并且不受任何系统限制。但是,它们太多对于调试来说是噩梦。简单表:

  1. 更少的锁意味着更多的争用(缓慢的系统调用,CPU停顿)和更少的并行性
  2. 更少的锁意味着更少的调试多线程问题。
  3. 更多的锁意味着更少的争用和更高的并行度
  4. 更多的锁意味着遇到不可调试的死锁的机会更大。

应该找到并维护一个平衡的应用锁定方案,通常使#2和#3保持平衡。


(*)锁定互斥量较少的问题是,如果您的应用程序锁定过多,则会导致大量CPU间/内核间流量从其他CPU的数据缓存中清除互斥体内存,以确保缓存一致性。缓存刷新就像轻量级中断一样,由CPU透明地处理-但是它们确实引入了所谓的停顿(搜索“停顿”)。

拖延是导致锁定代码运行缓慢的原因,通常没有任何明显的迹象表明应用程序运行缓慢的原因。(有些架构提供了CPU /内核之间的流量统计信息,有些则没有。)

为了避免该问题,人们通常求助于大量的锁,以减少锁争用的可能性并避免停顿。这就是为什么存在廉价的用户空间锁定(不受系统限制)的原因。


谢谢,这基本上回答了我的问题。我不知道内核(例如Linux内核)处理互斥锁,而您是通过syscalls控制它们的。但是,由于Linux本身管理调度和上下文切换,所以这很有意义。但是现在,我对互斥锁的内部锁定/解锁有一个粗略的想象。
艾伯特2010年

2
@阿尔伯特:哦。我忘了上下文切换...上下文切换太消耗性能了。如果锁获取失败并且线程必须等待,那将是上下文切换的一半。CS本身速度很快,但是由于CPU可能会被其他进程使用,因此缓存中将填充外来数据。在线程最终获得锁之后,机会是CPU必须重新从RAM重新加载几乎所有内容。
Dummy00001 2010年

@ Dummy00001切换到另一个进程意味着您必须更改CPU的内存映射。那不是那么便宜。
curiousguy19年

27

我想知道同一件事,所以我测量了它。在我的盒子(3.612361 GHz的AMD FX(tm)-8150八核处理器)上,锁定和解锁位于其自己的缓存行中并且已经缓存的未锁定互斥锁,需要47个时钟(13 ns)。

由于两个内核之间的同步(我使用的是CPU#0和#1),我只能在两个线程上每102 ns调用一次锁定/解锁对,因此每51 ns调用一次,由此可以得出结论,大约需要38秒钟ns在线程执行解锁后恢复,下一个线程可以再次锁定它之前。

我用来调查此问题的程序可以在这里找到:https : //github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx

请注意,它具有一些针对我的盒子的硬编码值(xrange,yrange和rdtsc开销),因此您可能必须先对其进行试验,然后它才能为您服务。

它在该状态下生成的图形为:

在此处输入图片说明

这显示了基准测试在以下代码上运行的结果:

uint64_t do_Ndec(int thread, int loop_count)
{
  uint64_t start;
  uint64_t end;
  int __d0;

  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
  mutex.lock();
  mutex.unlock();
  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
  asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
  return end - start;
}

这两个rdtsc调用测量锁定和解锁互斥锁所需的时钟数(我的盒子上rdtsc调用的开销为39个时钟)。第三个asm是一个延迟循环。线程1的延迟循环的大小比线程0的延迟循环的大小小1个计数,因此线程1的速度稍快一些。

在大小为100,000的紧密循环中调用上述函数。尽管该功能对于线程1而言稍快一些,但由于调用了互斥锁,所以两个循环都同步。从图中可以看出这一点,因为对于线程1,为锁定/解锁对测量的时钟数稍大一些,以解决线程1下的循环中较短的延迟。

在上图中,右下角的点是延迟loop_count为150的测量,然后跟随底部的点朝左,每次测量时都会将loop_count减一。当它变为77时,两个线程中每102 ns调用一次该函数。如果随后进一步减少loop_count,则不再可能同步线程,并且互斥锁在大多数时间实际上已开始被锁定,导致执行锁定/解锁所需的时钟量增加。因此,函数调用的平均时间也会增加。因此情节点现在又往右上移。

由此可以得出结论,每50 ns锁定和解锁一个互斥锁对我来说并不是问题。

总而言之,我的结论是,对OP问题的答案是,添加更多的互斥锁会更好,只要它导致较少的争用即可。

尝试将互斥锁锁定得尽可能短。将它们置于循环之外的唯一原因是,如果该循环的循环速度快于每100 ns(或者,希望同时运行该循环的线程数乘以50 ns)或13 ns次循环大小比通过争用得到的延迟更多。

编辑:我现在对该主题有了更多的了解,并开始怀疑我在这里提出的结论。首先,CPU 0和1被证明是超线程的。即使AMD声称有8个真实内核,但肯定会有一些麻烦,因为其他两个内核之间的延迟要大得多(即0和1成对,2和3、4和5以及6和7也是如此) )。其次,实现std :: mutex的方式是,在无法立即获得互斥锁的锁定(这无疑会非常慢)时,它会在实际执行系统调用之前旋转锁定一点。因此,我在这里测量的是绝对最理想的状态,实际上,每次锁定/解锁时,锁定和解锁可能要花费更多的时间。

最重要的是,互斥体是通过原子实现的。为了使内核之间的原子同步,必须锁定内部总线,该总线将冻结相应的高速缓存行几百个时钟周期。在无法获得锁的情况下,必须执行系统调用以使线程进入睡眠状态;这显然非常慢(系统调用的时间约为10毫秒)。通常,这并不是真正的问题,因为该线程无论如何都必须休眠-但这可能是争用较高的问题,其中线程在正常旋转时无法获得锁,因此系统调用也是如此,但是CAN之后不久拿起锁。例如,如果多个线程紧密地锁定和解锁互斥锁,并且每个线程将锁定保持1微秒左右,然后可能会由于不断地进入睡眠状态并再次醒来而大大放慢了速度。同样,一旦一个线程休眠并且另一个线程必须将其唤醒,则该线程必须执行系统调用,并被延迟约10微秒。因此,当另一个线程正在等待内核中的互斥锁解锁时(在旋转时间过长之后),在解锁互斥锁时会发生这种延迟。


10

这取决于您实际所谓的“互斥体”,操作系统模式等。

最低限度它是一个互锁内存操作的成本。这是一个相对繁重的操作(与其他原始汇编程序命令相比)。

但是,这可能会更高。如果您所谓的“互斥体”是一个内核对象(即,由OS管理的对象)并在用户模式下运行,则对它的每个操作都会导致内核模式事务,这非常繁重。

例如,在Intel Core Duo处理器Windows XP上。互锁操作:大约需要40个CPU周期。内核模式调用(即系统调用)-大约2000个CPU周期。

如果是这样,您可以考虑使用关键部分。它是内核互斥锁和互锁的内存访问的混合体。


7
Windows关键部分更接近互斥体。它们具有常规的互斥体语义,但是它们是本地过程的。最后一部分使它们更快,因为它们可以完全在您的过程中处理(因此可以在用户模式代码中处理)。
MSalters 2010年

2
如果还提供了常用操作(例如算术/ if-else / cache-miss / indirect)的CPU周期数进行比较,则该数字将更为有用。....如果有一些参考号码,那就更好了。在互联网上,很难找到这样的信息。
javaLover

@javaLover操作不会循环运行;它们在算术单元上运行了多个周期。完全不同。任何一条指令的时间成本不是一个确定的数量,只是资源使用成本。这些资源是共享的。内存指令的影响取决于大量缓存等
。– curiousguy

@curiousguy同意。我不清楚。我想要答案,例如std::mutex平均使用时间(以秒为单位)是的10倍以上int++。但是,我知道很难回答,因为它很大程度上取决于很多事情。
javaLover

6

成本将根据实现方式而有所不同,但您应牢记两件事:

  • 由于这是一项相当原始的操作,而且由于其使用方式(使用过 很多)。
  • 不管它有多昂贵,因为如果想要安全的多线程操作就需要使用它。如果需要它,那么就需要它。

在单处理器系统上,通常只可以禁用足够长的中断来原子地更改数据。多处理器系统可以使用测试设置策略。

在这两种情况下,指令都是相对有效的。

至于是为大型数据结构提供单个互斥锁,还是为每个部分提供多个互斥锁,那是一种平衡。

如果只有一个互斥锁,则在多个线程之间争用的风险更高。您可以通过在每个部分使用一个互斥锁来降低这种风险,但是您不想陷入线程必须锁定180个互斥锁来完成其工作的情况:


1
是的,但是效率如何?它是一条机器指令吗?或大约10点?还是100左右?1000?更多?所有这些仍然有效,但是在极端情况下可以有所作为。
艾伯特2010年

1
好吧,这完全取决于实现。您可以在大约六条机器指令中以循环方式关闭中断,测试/设置整数并重新激活中断。由于处理器趋向于将其作为单个指令提供,因此测试设置几乎可以完成很多。
paxdiablo 2010年

总线锁定的测试和设置是x86上的一条(相当长的)指令。其余使用它的机器都非常快(“测试是否成功?”这是CPU擅长快速完成的一个问题),但总线锁定指令的长度才是真正重要的,因为它是阻塞事物的一部分。带有中断的解决方案要慢得多,因为处理它们通常仅限于OS内核以阻止琐碎的DoS攻击。
Donal Fellows 2010年

顺便说一句,不要使用drop / reacquire作为让他人获得线程收益的方法;这是在多核系统上很糟糕的策略。(这是CPython弄错的相对很少的事情之一。)
Donal Fellows 2010年

@Donal:拖放/重新获取是什么意思?听起来很重要;你能给我更多的信息吗?
艾伯特2010年

4

我对pthread和互斥锁是完全陌生的,但是我可以通过实验确认,在没有竞争的情况下,锁定/解锁互斥锁的成本几乎是零,但在存在竞争时,阻塞的成本却非常高。我使用线程池运行了一个简单的代码,其中的任务只是计算由互斥锁保护的全局变量中的和:

y = exp(-j*0.0001);
pthread_mutex_lock(&lock);
x += y ;
pthread_mutex_unlock(&lock);

通过一个线程,该程序几乎瞬时(不到一秒)求和10,000,000个值;具有两个线程(在具有4核的MacBook上),相同的程序需要39秒。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.