30

一位同事最近遇到了一个问题,归结为我们认为具有两个线程的 C++ 应用程序中的以下事件序列:

  • 线程 A 持有一个互斥锁。

  • 当线程 A 持有互斥体时,线程 B 尝试锁定它。由于它被持有,线程 B 被挂起。

  • 线程 A 完成了它持有互斥锁的工作,从而释放了互斥锁。

  • 此后不久,线程 A 需要访问受互斥锁保护的资源,因此它再次锁定它。

  • 似乎线程 A 再次获得了互斥锁;线程 B 仍在等待,即使它首先“请求”锁定。

这一系列事件是否符合 C++11std::mutex和/或 pthread 的语义?老实说,我以前从未考虑过互斥锁的这一方面。

是否有任何公平保证可以防止其他线程饥饿太久,或者有什么方法可以得到这样的保证?

4

7 回答 7

29

已知问题。C++ 互斥锁是操作系统提供的互斥锁之上的薄层,操作系统提供的互斥锁通常是不公平的。他们不关心 FIFO。

同一枚硬币的另一面是线程通常不会被抢占,直到它们用完它们的时间片。结果,这种情况下的线程 A 很可能会继续执行,并因此立即获得互斥锁。

于 2016-06-16T13:33:23.423 回答
3

std::mutex 的保证是启用对共享资源的独占访问。它的唯一目的是消除多个线程尝试访问共享资源时的竞争条件。

出于性能原因,互斥锁的实现者可能会选择支持当前线程获取互斥锁(而不是另一个线程)。允许当前线程获取互斥体并在不需要上下文切换的情况下向前推进通常是分析/测量支持的首选实现选择。

或者,可以构造互斥锁以优先选择另一个(阻塞的)线程进行采集(可能根据 FIFO 选择)。这可能需要线程上下文切换(在相同或其他处理器内核上)增加延迟/开销。注意:FIFO 互斥体的行为方式令人惊讶。例如,在 FIFO 支持中必须考虑线程优先级 - 因此除非所有竞争线程具有相同的优先级,否则采集不会是严格的 FIFO。

在互斥锁的定义中添加 FIFO 要求会限制实现者在名义工作负载中提供次优性能。(看上面)

使用互斥锁保护可调用对象队列(std::function)将启用顺序执行。多个线程可以获取互斥体、将可调用对象入队并释放互斥体。可调用对象可以由单个线程执行(如果不需要同步,则可以由线程池执行)。

于 2016-06-17T21:25:21.997 回答
2

• 线程A 完成它持有互斥锁的工作,从而释放互斥锁。• 此后不久,线程 A 需要访问受互斥锁保护的资源,因此它再次锁定它

在现实世界中,当程序运行时。任何线程库或操作系统都不提供任何保证。这里的“不久之后”可能对操作系统和硬件意义重大。如果你说,2分钟,那么线程B肯定会得到它。如果您说 200 毫秒或更低,则无法保证 A 或 B 得到它。

内核数量、不同处理器/内核/线程单元上的负载、争用、线程切换、内核/用户切换、抢占、优先级、死锁检测方案等。人。会有很大的不同。仅仅从远处看绿色信号,你不能保证你会得到绿色。

如果你想让线程B必须获得资源,你可以使用IPC机制来指示线程B获得资源。

于 2016-06-16T15:56:55.457 回答
2

您无意中建议线程应该同步对同步原语的访问。顾名思义,互斥锁是关于互斥的。它们不是为控制流而设计的。如果你想用信号通知一个线程从另一个线程运行,你需要使用为控制流设计的同步原语,即信号。

于 2016-06-16T20:39:34.063 回答
2

这里的逻辑很简单——线程不是基于互斥锁抢占的,因为这需要为每个互斥锁操作产生成本,这绝对不是你想要的。获取互斥体的成本足够高,而不会强制调度程序寻找其他线程来运行。

如果你想解决这个问题,你总是可以产生当前线程。您可以使用 std::this_thread::yield() - http://en.cppreference.com/w/cpp/thread/yield - 这可能会为线程 B 提供接管互斥锁的机会。但在你这样做之前,请允许我告诉你,这是一种非常脆弱的做事方式,并且不能保证。或者,您可以更深入地调查该问题:

  1. 为什么A释放资源时B线程没有启动会出现问题?你的代码不应该依赖于这样的逻辑。

  2. 如果您确实需要这种逻辑,请考虑使用替代线程同步对象,例如屏障(boost::barrier 或http://linux.die.net/man/3/pthread_barrier_wait )。

  3. 调查一下你是否真的需要从 A 释放互斥锁 - 我发现锁定和快速释放互斥锁超过一次代码气味的做法,它通常会严重影响性能。看看您是否可以将数据提取分组到您可以使用的不可变结构中。

  4. 雄心勃勃,但尝试在没有互斥体的情况下工作 - 改用无锁结构和更实用的方法,包括使用许多不可变结构。我经常发现更新我的代码以不使用互斥锁可以获得相当大的性能提升(并且从 mt 的角度来看仍然可以正常工作)

于 2016-06-19T06:48:43.427 回答
2

您可以使用一个公平的互斥锁来解决您的任务,即保证您的操作的先进先出顺序的互斥锁。不幸的是,C++ 标准库没有公平的互斥锁。

值得庆幸的是,有一些开源实现,例如yamc(一个仅头文件的库)。

于 2021-03-27T16:45:00.307 回答
0

你怎么知道这个:

当线程 A 持有互斥体时,线程 B 尝试锁定它。由于它被持有,线程 B 被挂起。

你怎么知道线程 B 被挂起。你怎么知道不是刚刚完成了试图抢锁之前的那行代码,而是还没有抢到锁:

线程 B:

x = 17; // is the thread here?
// or here? ('between' lines of code)
mtx.lock();  // or suspended in here?
// how can you tell?

你说不出来。至少理论上不会。

因此,对于抽象机器(即语言)来说,获取锁的顺序是不可定义的。

于 2016-06-21T23:07:40.020 回答