为什么pthread互斥锁被认为比futex慢?

57
为什么 POSIX 互斥锁比 futex 重或慢?pthread 互斥锁类型的开销从哪里来?我听说 pthread 互斥锁基于 futex,在无争用时不会调用内核。看起来,pthread 互斥锁只是一个围绕 futex 的“包装器”。
这种开销是否仅在函数包装器调用和互斥锁函数需要“设置”futex (即基本上是为 pthread 互斥锁函数调用设置堆栈)时发生?或者 pthread 互斥锁中是否存在一些额外的内存屏障步骤?

3
当时你没有错过什么,因为它们当时并不存在!它们出现在Linux 2.6.x中(在2.5.x开发系列期间开发)。 - Nektarios
2
@Nektarios:实际上,早在很久以前就存在类似的锁。我相信最初的DRI锁(大约在91年,SGI)与当前的futexes类似。 - ninjalj
2
你有关于“POSIX互斥锁被认为比futexes更重或更慢”的参考资料吗?因为据我所知,自从NPTL以来,Linux上的pthread在过去几年中一直按照您描述的方式工作。 - Nemo
@Nemo:只是好奇,如果它们按照我在问题中描述的方式工作(即,在未争用时两者都保留在用户空间,并且在争用时都进行内核调用),那么为什么要费力使用 futex 而不是 mutex? - Jason
@Nemo:顺便说一句,在这个帖子上,人们似乎很喜欢我建议使用futex而不是mutex来回答问题,因为我的假设是即使在无争用的情况下,mutex也必须向内核发出调用。但我注意到一些帖子提到,在Linux上,pthread mutexes和semaphores是futexes的包装器,这就引出了一个问题:既然有更高级别的抽象可用,为什么要费心使用futex呢? - Jason
显示剩余6条评论
5个回答

34

Futexes是为了提高pthread mutexes的性能而创建的。NPTL使用futexes,而LinuxThreads早于futexes,这就是"较慢"考虑的原因所在。NPTL互斥锁可能会有一些额外开销,但不应该很大。

编辑: 实际开销主要包括:

  • 选择互斥锁类型的正确算法(普通、递归、自适应、错误检查;normal、robust、priority-inheritance、priority-protected),其中代码向编译器暗示我们可能正在使用普通互斥锁(因此它应该传递给CPU的分支预测逻辑),
  • 如果成功获取互斥锁,则写入当前持有者,这通常应该很快,因为它与我们刚刚获取的实际锁位于同一缓存线上,除非锁被强烈竞争,并且其他CPU在我们获取锁后访问了该锁并在我们尝试写入所有者时进行了操作(对于普通互斥锁来说,这个写操作是不需要的,但对于错误检查和递归互斥锁是必需的)。

因此,一些周期(典型情况)到几个周期 + 分支误判 + 附加缓存未命中(非常糟糕的情况)。


那么,在Linux上使用futex而不是mutex有意义吗? - Jason
2
@Jason:除非你正在编写自己的libc,或想要创建一些互斥量之外的同步原语,否则不是很必要。 - ninjalj
6
@Jason,futex 仅仅被视为 mutex 的替代品,并没有在性能上给你带来太大的不同。相反,它具有更难捕捉的 API,因此不要这样做。当 futex 被视为一起使用 mutex 和条件变量(在一个 int 条件下)时,它真正有用的地方在于,它更加时间和空间有效,并且避免了 POSIX 的陷阱,即某人可以使用不同的 mutex 来等待条件。 - Jens Gustedt
简而言之,什么时候使用哪个? - Eric

16

从技术上讲,pthread互斥锁与futex的速度没有快慢之分。pthread只是标准API,因此它们是快还是慢取决于该API的实现。

特别是在Linux中,pthread互斥锁是以futex的形式实现的,因此非常快速。实际上,您不希望直接使用futex API,因为它很难使用,在glibc中也没有适当的封装函数,并且需要编写汇编代码,这将使其不可移植。幸运的是,glibc维护者已经在pthread互斥锁API的底层为我们编写了所有这些内容。

现在,由于大多数操作系统没有实现futexes,因此程序员通常意味着pthread互斥锁是从常规pthread互斥锁实现中获得的性能,这是较慢的。

因此,统计事实上,在大多数符合POSIX标准的操作系统中,pthread互斥锁在内核空间中实现,并且比futex慢。在Linux中,它们的性能相同。可能有其他操作系统在用户空间(在未竞争情况下)中实现pthread互斥锁,因此具有更好的性能,但我目前只知道Linux。


“在大多数符合POSIX标准的操作系统中,pthread mutex是在内核空间中实现的”这是“统计事实”吗?真的吗?因为我认为很可能恰恰相反——很明显,如果可能的话,您会希望避免内核转换,因此我期望typical pthreads实现的作者尽可能地留在用户空间。” - al45tair
这意味着在“无争用”情况下,你只能停留在用户空间,即:
  • 当你到达一个互斥锁并且它是开放的,因此你不需要进入睡眠状态。
  • 当你释放一个互斥锁并发现没有人在等待它,因此你不需要唤醒任何人。
- Mark Veltzer
不,那实际上是一个错误。例如:你想知道互斥锁当前是否被持有。你该怎么做?用户空间只有互斥锁的ID,但互斥锁的所有状态都保存在内核空间中,而内核空间对用户空间是不可见的。如果互斥锁的状态暴露给用户空间,那么用户空间可能会破坏互斥锁的状态并导致内核崩溃(违反了用户空间/内核关系和操作系统定义的规定)。因此需要新的API。 - Mark Veltzer
不,这不是错误。使用类似futex的API可能是一种有效的方法,但例如在10.10之前,macOS的pthread_mutex实现使用Mach信号量进行阻塞,并在用户空间中保持其状态。我完全同意futex和当前macOS中的__psync_mutexdrop/wait API是特殊的API,顺便说一句,但我没有看到你提出的任何论点阻止了在10.10之前的macOS中看到的方法。 - al45tair
我非常怀疑信号量的状态是否被保留在用户空间。你的意思是我可以玩弄等待信号量的进程列表吗?从列表中删除一些进程?添加那些不想等待信号量的进程?请解释一下。即使在今天使用futexes,仍然需要仔细划分互斥锁的状态,以避免真正糟糕的竞态条件和攻击... - Mark Veltzer
显示剩余5条评论

16

简单回答你的问题,即futex已知其实现尽可能高效,而pthread mutex可能会有或可能没有。 至少pthread mutex具有确定mutex类型的开销,而futex则没有。 因此,除非有人想出比futex更轻的结构,然后发布一个使用该结构作为默认mutex的pthread实现,否则futex几乎总是至少与pthread mutex一样有效。


11
因为它们尽可能地停留在用户空间,这意味着它们需要较少的系统调用,这本质上更快,因为用户和内核模式之间的上下文切换是昂贵的。
我假设你谈论 POSIX 线程时指的是内核线程。完全可以使用 entirely userspace 实现 POSIX 线程,这不需要系统调用,但会有其他自身问题。
我的理解是 futex 在内核 POSIX 线程和用户空间 POSIX 线程之间处于中间状态。

1
即使 futex 没有争用,它仍然必须进行系统调用。如果无争用时 futex 和 mutex 都停留在用户空间,当它们争用时,mutex 需要进行哪些“额外”的系统调用而 futex 不需要呢?你是说 mutex 的开销在于它处理争用情况的方式(即与 futex 相比更复杂的内核调用)吗? - Jason
4
当内核互斥锁没有竞争时,它不会停留在用户空间,而是进入内核模式。在 POSIX 线程的内核实现中,任何线程操作都直接进入内核模式,因为该实现没有用户空间部分。 - Nektarios
2
为了使事情更复杂(我希望更明确),仅仅因为你正在使用“pthreads”或POSIX线程,并不意味着你正在使用内核或用户态实现。实际上,除非进行源码调查或实验观察其行为,否则我不知道如何确定这一点。 - Nektarios
1
我最初也认为即使在无争用情况下,内核线程的互斥锁始终会进行内核调用,但有人告诉我,在Linux上,这并不是真的,因为Linux pthread互斥锁和信号量是futexes的包装器,在无争用情况下仍然停留在用户空间。所以这让我有点困惑...我不明白为什么我们要费力处理原始的futexes,当互斥锁很容易使用,并且如果它们基于futexes,应该表现出完全相同的性能特征。 - Jason
1
“kernel” 互斥锁在 Linux 中不存在,只有 futexes。 - Spudd86
显示剩余2条评论

1
在AMD64上,futex是4个字节,而NPTL pthread_mutex_t是56个字节!是的,存在显着的开销。

5
我认为问题是关于运行时间性能而不是结构体大小。此外,用户空间需要更多的数据,在内核空间中有更多的数据可以用于futex,因此futex不是4字节,绝对不止这么少。 - Mark Veltzer

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