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

Alb*_*ert 134 multithreading mutex locking blocking

在低级语言(C,C++或其他)中:我可以选择在拥有一堆互斥(如pthread给我或者本机系统库提供的内容)或者对象的单个互斥之间.

锁定互斥锁的效率如何?即可能有多少汇编指令,以及它们花了多少时间(在互斥锁解锁的情况下)?

互斥量需要多少钱?真的有很多互斥体是一个问题吗?或者我可以在代码中抛出尽可能多的互斥变量,因为我有int变量并且它并不重要?

(我不确定不同硬件之间有多大差异.如果有,我也想了解它们.但大多数情况下,我对常见的硬件感兴趣.)

关键是,通过使用许多互斥体,每个互斥体只覆盖对象的一部分而不是整个对象的单个互斥体,我可以安全地使用许多块.我想知道我应该走多远.即我应该尽可能地尝试保护任何可能的块,无论多么复杂和多少互斥量这意味着什么?


关于锁定的WebKits博客文章(2016)与此问题非常相关,并解释了自旋锁,自适应锁,futex等之间的差异.

Dum*_*001 104

我可以选择在一堆互斥锁之间或者为一个对象放置一个互斥锁.

如果你有很多线程并且经常访问对象,那么多个锁会增加并行性.以可维护性为代价,因为更多的锁定意味着更多的锁定调试.

锁定互斥锁的效率如何?即可能有多少汇编指令,以及它们花了多少时间(在互斥锁解锁的情况下)?

精确的汇编程序指令是互斥锁的最小开销- 内存/缓存一致性保证是主要的开销.并且通常会采取特定的锁定 - 更好.

互斥体由两个主要部分组成(过度简化):( 1)指示互斥锁是否被锁定的标志和(2)等待队列.

更改标志只是很少的指令,通常在没有系统调用的情况下完成.如果互斥锁被锁定,系统调用将发生将调用线程添加到等待队列并开始等待.如果等待队列为空,则解锁是便宜的,但是否则需要系统调用来唤醒其中一个等待进程.(在某些系统上,使用廉价/快速系统调用来实现互斥锁,只有在争用的情况下,它们才会变为慢速(正常)系统调用.)

锁定解锁的互斥锁真的很便宜.解锁没有争用的互斥锁也很便宜.

互斥量需要多少钱?真的有很多互斥体是一个问题吗?或者我可以在我的代码中抛出尽可能多的互斥变量,因为我有int变量,这并不重要?

您可以根据需要在代码中输入尽可能多的互斥变量.您只受应用程序可以分配的内存量的限制.

摘要.用户空间锁(特别是互斥锁)很便宜,不受任何系统限制.但是他们中有太多都是调试的噩梦.简单表:

  1. 较少的锁意味着更多的争用(慢速系统调用,CPU停顿)和较少的并行性
  2. 减少锁定意味着调试多线程问题的问题更少.
  3. 更多的锁意味着更少的争用和更高的并行性
  4. 更多的锁意味着更多的机会陷入无法攻击的死锁.

应找到并维护应用程序的平衡锁定方案,通常平衡#2和#3.


(*)常常锁定互斥锁的问题是,如果你的应用程序中有太多的锁定,它会导致大部分CPU /核心流量从其他CPU的数据缓存中刷新互斥锁内存,以保证缓存一致性.缓存刷新就像轻量级中断,并由CPU透明地处理 - 但它们确实引入了所谓的停顿(搜索"停顿").

并且停顿是使锁定代码运行缓慢的原因,通常没有任何明显的指示为什么应用程序很慢.(某些arch提供CPU /核心流量统计信息,有些则不提供.)

为了避免这个问题,人们通常会使用大量的锁来降低锁定争用的可能性并避免失速.这就是为什么存在廉价用户空间锁定而不受系统限制的原因.

  • @Albert:哦.我忘记了上下文切换...上下文切换太耗费性能.如果锁获取*失败*并且线程必须等待,那就是上下文切换的一半.CS本身速度很快,但由于CPU可能被其他进程使用,因此缓存将充满外来数据.在线程最终获得锁之后,很可能CPU必须重新从RAM重新加载所有内容. (2认同)
  • 许多小锁不会使事情变得更复杂,特别是当它们被持有的时间很短时。然而,当您不可避免地必须嵌套它们时,拥有更少、更大的锁会使事情变得更加复杂。因此,我真的不同意“更多的锁意味着更多的机会遇到不可调试的死锁”。 (2认同)

Car*_*ood 20

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

由于两个内核之间的同步(我使用了CPU#0和#1),我只能在两个线程上每102 ns调用一次锁定/解锁对,因此每51 ns一次,从中可以得出结论大约需要38 ns 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;
}
Run Code Online (Sandbox Code Playgroud)

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

上述函数在大小为100,000的紧密循环中调用.尽管线程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的实现方式是,在实际进行系统调用之前,当它无法立即获取互斥锁(这无疑会非常慢)时,它会旋转一点.所以我在这里测量的是绝对最理想的情况,实际上锁定和解锁可能需要每次锁定/解锁时间更长.

最重要的是,互斥体是用原子实现的.要在内核之间同步原子,必须锁定内部总线,这会冻结相应的高速缓存行数百个时钟周期.在无法获得锁定的情况下,必须执行系统调用以使线程进入休眠状态; 这显然非常缓慢.通常这不是一个真正的问题,因为该线程无论如何都必须睡眠 - 但它可能是一个高争用的问题,其中线程无法在正常旋转的时间内获得锁定,系统调用也是如此,但CAN之后很快拿到锁.例如,如果几个线程在一个紧密的循环中锁定和解锁互斥锁并且每个都保持锁定1微秒左右,那么它们可能会因为它们不断地进入睡眠并再次唤醒而大大减慢速度.


val*_*ldo 10

这取决于你实际称之为"互斥",操作系统模式等.

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

但是,这可能会高得多.如果你称之为"互斥"是一个内核对象(即 - 由操作系统管理的对象)并在用户模式下运行 - 它上面的每个操作都会导致内核模式事务,这非常繁重.

例如在英特尔酷睿双核处理器,Windows XP上.互锁操作:大约需要40个CPU周期.内核模式调用(即系统调用) - 大约2000个CPU周期.

如果是这种情况 - 您可以考虑使用关键部分.它是内核互斥锁和互锁内存访问的混合体.

  • Windows关键部分更接近互斥锁.它们具有常规的互斥语义,但它们是进程本地的.最后一部分使它们更快,因为它们可以在您的过程中完全处理(因此也就是用户模式代码). (7认同)
  • 如果还提供了常用操作(例如,算术/ if-else / cache-miss / indirect)的CPU周期数进行比较,则该数字将更为有用。....如果有一些参考号码,那就更好了。在互联网上,很难找到这样的信息。 (2认同)

Gra*_*tty 8

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

y = exp(-j*0.0001);
pthread_mutex_lock(&lock);
x += y ;
pthread_mutex_unlock(&lock);
Run Code Online (Sandbox Code Playgroud)

使用一个线程,该程序几乎立即(不到一秒)汇总了 10,000,000 个值;使用两个线程(在 4 核 MacBook 上),相同的程序需要 39 秒。


pax*_*blo 6

成本将根据实施情况而有所不同,但您应该记住两件事:

  • 成本最有可能是最小的,因为它既是一个相当原始的操作,并且由于其使用模式(使用很多)将尽可能地进行优化.
  • 如果你想要安全的多线程操作,你需要使用它并不重要.如果你需要它,那么你需要它.

在单处理器系统上,通常只需禁用中断足够长的时间以原子方式更改数据.多处理器系统可以使用测试和设置策略.

在这两种情况下,指令都相对有效.

至于你是否应该为大规模数据结构提供单个互斥锁,或者是否有多个互斥锁,每个部分都有一个,这是一种平衡行为.

通过使用单个互斥锁,您在多个线程之间存在更高的争用风险.您可以通过每个部分使用互斥锁来降低此风险,但是您不希望陷入线程必须锁定180个互斥锁才能完成其工作的情况:-)

  • 是的,但是*如何*有效?是单条机器指令吗?或者大约10个?还是100左右?1000?更多的?所有这些仍然有效,但在极端情况下会有所作为。 (2认同)