锁定和解锁互斥的效率如何?互斥的代价是什么?

在低级语言(C、C++或其他语言)中:我可以选择使用一组互斥锁(如pthread提供的互斥锁或本机系统库提供的任何互斥锁),也可以选择为对象使用单个互斥锁。

锁定互斥锁的效率如何?例如,可能有多少条汇编指令,它们需要多少时间(在互斥被解锁的情况下)?

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

(我不确定不同硬件之间的差异有多大。如果有的话,我也想了解一下。但我最感兴趣的是常见硬件。

关键是,通过使用许多互斥锁,每个互斥锁只覆盖对象的一部分,而不是整个对象的单个互斥锁,我可以保护许多块。我想知道我应该走多远。也就是说,我是否应该尽可能地保护任何可能的块,无论这意味着多复杂和多多互斥?


WebKits博客文章(2016)关于锁定与这个问题密切相关,它解释了自旋锁、自适应锁、Futex等之间的区别。

86254 次浏览

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

  • 成本很可能是最小的,因为它既是一个相当原始的操作,又由于其使用模式(使用许多)而被尽可能地优化。
  • 如果你想要安全的多线程操作,你需要使用它,所以它有多贵并不重要。如果你需要它,那你就需要它。

在单处理器系统上,您通常可以禁用中断足够长的时间来自动更改数据。多处理器系统可以使用测试和设置策略。

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

至于你是否应该为一个庞大的数据结构提供一个互斥锁,或者有许多互斥锁,它的每个部分一个,这是一个平衡的行为。

如果只有一个互斥,那么多个线程之间发生争用的风险就会更高。您可以通过为每个部分设置一个互斥锁来降低这种风险,但您不希望陷入这样一种情况,即线程必须锁定180个互斥锁才能完成其工作:-)

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

最小量处,它是互锁存储器操作的成本。这是一个相对繁重的操作(与其他原始汇编命令相比)。

然而,这可能要高得多。如果您所称的“互斥”是一个内核对象(即由操作系统管理的对象),并在用户模式下运行,则对它的每个操作都会导致内核模式事务,该事务非常

例如

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

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

对于一个对象,我可以选择使用一组互斥锁,也可以选择使用单个互斥锁。

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

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

精确的汇编指令是互斥-__ABC的最小开销,__1保证是主要开销。并且更少地使用特定的锁-更好。

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

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

锁定未锁定的互斥锁真的很便宜。在没有竞争的情况下解锁互斥也很便宜。

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

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

总结。用户空间锁(特别是互斥锁)很便宜,而且不受任何系统限制。但如果数量太多,就会成为调试的噩梦。简单表格:

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

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


(*)不经常锁定互斥锁的问题是,如果您的应用程序中有太多的锁定,则会导致大量CPU/核心间流量从其他CPU的数据缓存中刷新互斥锁内存,以保证缓存一致性。缓存刷新类似于轻量级中断,由CPU透明地处理,但它们确实引入了所谓的摊位(搜索“ stall ”)。

这些停顿使得锁定代码运行缓慢,通常没有任何明显的迹象表明应用程序为什么运行缓慢。(有些ARCH提供CPU/核心间的流量统计,有些则不提供。)

为了避免这个问题,人们通常采用大量的锁来减少锁竞争的概率,从而避免延迟。这就是为什么存在不受系统限制的廉价用户空间锁定的原因。

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

由于两个核心之间的同步(我使用CPU#0和#1), 我只能在两个线程上每102ns调用一次锁定/解锁对, 因此,每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 count,因此线程1稍快。

上述函数在大小为100,000的紧密循环中调用。尽管线程1的函数稍快一些,但由于对互斥的调用,两个循环都同步了。这在图中是可见的,因为对于线程1,为锁定/解锁对测量的时钟数稍大,以说明其下面的循环中的较短延迟。

在上图中,右下角的点是延迟环路_计数为150的测量,然后跟随底部的点,向左,环路_计数在每次测量中减少1。当它变为77时,该函数在两个线程中每102 ns调用一次。如果随后的循环_计数进一步减少,则不再可能同步线程,并且互斥体开始在大多数时间被实际锁定,从而导致执行锁定/解锁所需的时钟量增加。函数调用的平均时间也会因此而增加。所以绘图点现在上升并再次向右移动。

由此我们可以得出结论,在我的机器上,每50ns锁定和解锁一个互斥锁不是问题。

总而言之,我的结论是,OP问题的答案是,只要能减少争用,添加更多的互斥锁就更好。

尝试锁定尽可能短的互斥锁。将它们放在循环之外的唯一原因是,如果该循环的循环速度快于每100ns一次(或者更确切地说,想要同时运行该循环的线程数乘以50ns),或者当13ns乘以循环大小的延迟大于您通过争用获得的延迟时。

编辑:我现在对这个主题有了更多的了解,并开始怀疑我在这里提出的结论。首先,CPU 0和1是超线程的;即使AMD声称有8个真正的核心,但肯定有一些非常可疑的东西,因为其他两个核心之间的延迟要大得多(即,0和1形成一对,2和3,4和5,6和7也是如此)。其次,STD:mutex的实现方式是,当它无法立即获得互斥锁时(这无疑会非常慢),它会在实际执行系统调用之前自旋锁一段时间。所以我在这里测量的是绝对最理想的情况,在实践中,每次锁定和解锁可能需要更多的时间。

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

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

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

对于一个线程,程序几乎在瞬间(不到一秒钟)对10,000,000个值求和;使用两个线程(在4核的MacBook上),同样的程序需要39秒。

我刚刚在我的Windows 10系统上测量了它。

这是测试完全没有争用的单线程代码。

编译器:Visual Studio 2019,x64版本,从测量中减去循环开销。

使用std::mutex大约需要74个机器周期,而使用本地Win32CRITICAL_SECTION大约需要53个机器周期。

因此,除非与代码本身相比,100个机器周期是相当长的时间,否则互斥体不会成为性能问题的根源。