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

时间:2010-09-06 14:00:32

标签: multithreading locking mutex blocking

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

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

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

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

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


WebKits blog post (2016) about locking与此问题非常相关,并解释了自旋锁,自适应锁,futex等之间的差异。

5 个答案:

答案 0 :(得分:97)

  

我可以选择使用一堆互斥锁,也可以选择一个互斥锁。

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

  

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

精确的汇编程序指令是a mutex的最小开销 - the memory/cache coherency保证是主要的开销。并且通常会采取特定的锁定 - 更好。

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

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

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

  

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

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

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

  1. 减少锁定意味着更多争用(慢速系统调用,CPU停顿)和更少的并行性
  2. 减少锁定意味着调试多线程问题的问题更少。
  3. 更多锁意味着更少的争用和更高的并行性
  4. 更多的锁意味着更多的机会遇到无法释放的死锁。
  5. 应找到并维护应用程序的平衡锁定方案,通常平衡#2和#3。


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

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

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

答案 1 :(得分:12)

我想知道同样的事情,所以我测量了它。 在我的盒子(AMD FX(tm)-8150八核处理器,3.612361 GHz), 锁定和解锁位于其自身缓存行中且已缓存的未锁定互斥锁,需要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开销),所以你可能需要先试验它才能适合你。

它在该状态下生成的图表是:

enter image description here

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

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,锁定/解锁对测量的时钟数量略大,以解决其下方循环中较短的延迟。

在上图中,右下角是延迟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的实现方式是,在实际进行系统调用之前,当它无法立即获取互斥锁(这无疑会非常慢)时,它会旋转一点。所以我在这里测量的是绝对最理想的情况,实际上锁定和解锁可能需要每次锁定/解锁时间更长。

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

答案 2 :(得分:10)

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

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

然而,这可能会高得多。如果你将“mutex”称为内核对象(即 - 由OS管理的对象)并在用户模式下运行 - 它上面的每个操作都会导致内核模式事务,非常重。< / p>

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

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

答案 3 :(得分:6)

费用会因实施而异,但您应该记住两件事:

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

在单处理器系统上,通常只需禁用中断足够长的时间以原子方式更改数据。多处理器系统可以使用test-and-set策略。

在这两种情况下,说明都相对有效。

至于你是应该为一个海量数据结构提供一个互斥锁,还是有多个互斥锁,每个部分都有一个,这是一种平衡行为。

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

答案 4 :(得分:1)

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

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

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