PTHREAD_MUTEX_ADAPTIVE_NP是什么意思?

31

我在哪里可以找到关于“自适应”pthread互斥锁的文档?在我的系统上定义了PTHREAD_MUTEX_ADAPTIVE_NP符号,但我在唯一能找到的文档中找不到有关自适应互斥锁是什么或何时适合使用的信息。

那么,它是什么,何时应该使用它呢?

供参考,我使用的libc版本是:

GNU C Library (Ubuntu EGLIBC 2.15-0ubuntu10.5) stable release version 2.15, by Roland McGrath et al.
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.6.3.
Compiled on a Linux 3.2.50 system on 2013-09-30.
Available extensions:
    crypt add-on version 2.1 by Michael Glad and others
    GNU Libidn by Simon Josefsson
    Native POSIX Threads Library by Ulrich Drepper et al
    BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.

而且 "uname -a" 给出

Linux desktop 3.2.0-55-generic #85-Ubuntu SMP Wed Oct 2 12:29:27 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
3个回答

76

PTHREAD_MUTEX_ADAPTIVE_NP是我在担任glibc贡献者期间发明的一种东西1,旨在使LinuxThreads更可靠且性能更好。LinuxThreads是glibc的前身,最初由Xavier Leroy开发为一个独立库,他也是OCaml的创造者之一。

自适应互斥锁在NTPL中基本上保持不变:代码几乎完全相同,包括用于估计平滑和相对于估计的最大自旋的魔术常数。

在SMP下,当您尝试获取一个已被锁定的互斥锁时,简单地放弃并调用内核进行阻塞可能不是最佳选择。如果锁的持有者仅持有锁几条指令,那么等待执行这些指令,然后使用原子操作获取锁比通过进行系统调用花费数百个额外周期要更便宜。

内核开发人员非常了解这一点,这也是为什么在Linux内核中有自旋锁用于快速关键部分的原因之一。(当然,其他原因之一是,不能休眠的代码,因为它处于中断上下文中,可以获取自旋锁。)
问题是,你应该等多久?如果你一直自旋直到锁被获取,那可能是次优的。用户空间程序不像内核代码那样编写得很好(咳咳)。它们可能有很长的关键部分。它们也不能禁用抢占;有时关键部分会因为上下文切换而崩溃。(POSIX线程现在提供了实时工具来处理这个问题:你可以将线程放入实时优先级和FIFO调度等,还可以配置处理器亲和性。)
我觉得我们尝试过固定的迭代次数,但后来我有了一个想法:为什么我们要猜测,而不是测量呢?为什么我们不实现一个平滑的锁持续时间估计器,类似于我们对TCP重传超时(RTO)估计器的做法呢?每次我们在一个锁上自旋时,我们应该测量实际需要多少次自旋才能获取到它。此外,我们不应该永远自旋下去:也许我们只需要自旋不超过当前估计值的两倍。当我们进行测量时,我们可以以指数方式平滑它,只需要几条指令:取先前值的一部分和新值的一部分,然后将它们相加,这相当于将它们的差的一部分加回到估计器中:比如说,对于旧值和新值之间的1/8到7/8的混合,estimator += (new_val - estimator)/8
你可以把这看作是一个看门狗。假设估计器告诉你,平均需要80次自旋才能获取到锁。那么,如果你已经执行了160次自旋,你可以相当有把握地认为出现了问题:锁的持有者正在执行一些异常长的操作,或者可能发生了页面错误或其他被抢占的情况。此时,等待的线程会减少损失并调用内核进行阻塞。
没有测量,你无法准确地做到这一点:没有“一刀切”的价值。比如说,对于一个关键部分非常短暂的程序来说,固定的200次旋转限制是次优的,因为在等待仅仅10次旋转后,几乎总是可以获取到锁。互斥锁函数每次出现异常等待时间时都会消耗200次迭代,而不是在20次时优雅地放弃并节省循环。
这种自适应的方法是专门针对特定情况的,也就是说,它并不适用于所有程序中的所有锁,因此它被打包成了一种特殊的互斥锁类型。例如,对于长时间锁定互斥锁的程序来说,它的效果并不好:这些时间长到大估计值上旋转浪费的CPU时间比进入内核浪费的时间还要多。这种方法也不适用于单处理器:除了试图获取锁的线程之外,所有其他线程都会在内核中挂起。这种方法也不适用于重视公平性的情况:它是一种机会主义的锁。无论有多少其他线程在等待,无论等待多长时间,或者它们的优先级如何,新的线程都可以来抢夺锁。
如果你的代码非常规范,关键部分很短且高度争用,并且你希望在SMP上获得更好的性能,那么自适应互斥锁可能值得一试。
这个工作可以在Git历史中找到。更新linuxthreads/ChangeLog文件的提交在这里;实际的更改与另一个作者对charmap.c文件的无关更改混在一起,可以在这里找到。

在源代码中,我看到它使用nops进行忙等待。你为什么不像WebKit那样使用sched_yield进行忙等待呢?有什么原因吗? - Hongli
@HongLi 在循环中使用 sched_yield 已经过时。Linux 大约15年前开发了 futexes。使用futex,我们可以使用原子操作(无需内核干预)来获取锁。如果失败,我们可以进入内核等待 futex(而不是执行没有等待任何东西的费时的 sched_yield 调用)。我们还可以在 futex 上旋转:也就是,在调用 futex 等待操作之前,尝试多次执行原子操作。WebKit方法已经过时,基于过时的概念。 - Kaz
@Kaz 感谢您的回复。我知道 sched_yield 不会等待任何东西,但 WebKit 博客文章认为他们在 sched_yield 上忙等以优化微争用情况:即关键部分很短的争用情况。如果 sched_yield 的常数时间开销比 futex 系统调用小,那么在微争用情况下忙等 sched_yield 几次是有益的,这对于优化来说是合理的。您同意这个观点吗?如果不是,那么您是否在说 futex 系统调用的常数开销非常低? - Hongli
@Hongli,使用futex时,您需要在锁定线程和解锁线程中都进行系统调用。而使用sched_yield时,您只需要在锁定线程中进行一次系统调用。问题在于,sched_yield是不可预测的,并且可能会因为调度程序对从一个核心迁移到另一个核心(在缓存传输时间方面)犹豫不决而给您带来不同的结果。 - user1143634

7

那里提到了这个符号:

http://elias.rhi.hi.is/libc/Mutexes.html

"LinuxThreads仅支持一种互斥锁属性:互斥锁类型,可以是“快速”互斥锁PTHREAD_MUTEX_ADAPTIVE_NP,也可以是“递归”互斥锁PTHREAD_MUTEX_RECURSIVE_NP、 “定时”互斥锁PTHREAD_MUTEX_TIMED_NP或“错误检查”互斥锁PTHREAD_MUTEX_ERRORCHECK_NP。如NP后缀所示,这是POSIX标准的非可移植扩展,不应在可移植程序中使用。
互斥锁类型决定了线程尝试使用pthread_mutex_lock锁定已经拥有的互斥锁时会发生什么。如果互斥锁是“快速”类型,则pthread_mutex_lock会永久挂起调用线程。如果互斥锁是“错误检查”类型,则pthread_mutex_lock会立即返回错误代码EDEADLK。如果互斥锁是“递归”类型,则对pthread_mutex_lock的调用会立即返回成功的返回代码。拥有互斥锁的线程已经将其锁定的次数记录在互斥锁中。拥有互斥锁的线程必须调用pthread_mutex_unlock相同次数,然后互斥锁才能返回到未锁定状态。
默认的互斥锁类型是“定时”,即PTHREAD_MUTEX_TIMED_NP。”

关于互斥标志和PTHREAD_MUTEX_ADAPTIVE_NP,可以在这里找到更多信息:

"PTHRED_MUTEX_ADAPTIVE_NP是一种新的互斥锁,旨在通过牺牲公平性甚至CPU周期来实现高吞吐量。此互斥锁不会将所有权转移给等待线程,而是允许竞争。此外,在SMP内核上,锁定操作使用自旋来重试锁定,以避免立即取消调度的成本。"

基本上建议如下:在需要高吞吐量的情况下,可以实现这种互斥锁,需要考虑线程逻辑的额外考虑,因为它的本质。您将不得不设计一个算法,可以使用这些属性,从而实现高吞吐量。自我负载平衡(而不是“来自内核”)的东西,执行顺序不重要。

有一本非常好的Linux / Unix多线程编程书籍,但我忘记了书名。如果我找到它,我会更新。


是的,我本来就想在我的问题中链接到那个页面,已经修复了。我的问题是它并没有告诉我每种类型是什么或者做什么。而且在页面下方,它给人的印象是唯一的区别就是当线程尝试重新锁定互斥量时会发生什么。我相信还有其他的区别。 - laslowh
嗯,对我来说,页面上的下面一段话看起来很清楚。选择的值会改变pthread_mutex_lock函数在应用于由调用线程持有的互斥锁时的行为。(根据答案进行了编辑) - Sebastien
2
清晰但不完整。在这种情况下,“定时”互斥锁的行为如何?互斥锁类型之间是否还有其他区别?如果其定义特征是在多个锁定时死锁线程,那么为什么称其为“自适应”或“快速”互斥锁? - laslowh

2

这里。据我所读,它是一个极其简单的互斥锁,只关心让无竞争情况下运行更快,而不关心其他任何事情。


你介意我把你的答案和我的合并吗?我至少会复制粘贴相关段落在这里。感谢提供信息! - Sebastien
@Sebastien 很高兴能帮忙,直接上吧。根据我的了解,虽然我没有深入挖掘,但这个文档应该是源头。 - jthill
@jthill:不错的发现。令人惊讶的是,我们能够共同找到的唯一文档是Linux内核列表上的一封电子邮件。 - laslowh
这只是一个链接回答,恐怕不太够。 - Suma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接