【问题标题】:Do mutexes guarantee ordering of acquisition? Unlocking thread takes it again while others are still waiting互斥锁是否保证获取顺序?解锁线程再次使用它,而其他人仍在等待
【发布时间】:2016-06-16 13:32:04
【问题描述】:

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

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

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

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

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

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

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

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

【问题讨论】:

标签: c++ multithreading pthreads mutex contention


【解决方案1】:

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

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

【讨论】:

  • 另一种看法是,当A首先放弃互斥锁时,操作系统只是将B标记为准备运行。操作系统实际上不会执行上下文切换,直到它绝对必须这样做,因为它很昂贵。它发生在(如您所说)A 的时间片已过期,或者 A 调用阻塞函数,或者 B 具有比 A 更高的优先级(当 A 放弃互斥锁时由操作系统评估)。如果您开始使用线程优先级来让事情以正确的顺序发生,那么您可能需要一个解决优先级反转的操作系统(所以不要使用 Linux 或 Windows。Linux+PREMPT_RT 就可以了)。
  • Windows 本机同步类型(CreateMutex、CreateSemaphore、WaitForSingleObject、WaitForMultipleObjects)使用某种类型的队列,以便根据实际测试以 FIFO 顺序授予请求,但我还没有发现此文档。
【解决方案2】:

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

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

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

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

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

【讨论】:

    【解决方案3】:

    •线程 A 完成了它持有互斥锁的工作,因此 释放互斥锁。 • 此后不久,线程 A 需要接触一个资源,该资源是 受互斥体保护,所以它再次锁定它

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

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

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

    【讨论】:

    • 200 毫秒仍然是永远的,比调度时间片长得多,并且比解锁唤醒任何睡眠线程的内核间延迟(通常为 40 到 80 纳秒)要长得多。即使是 200 微秒也是相当长的时间,除非所有其他内核已经在运行,并且操作系统选择让它们在唤醒等待锁定的线程之前完成它们的时间片。 (大多数现代操作系统都有操作系统辅助的睡眠/唤醒锁定,而不仅仅是唤醒轮询。)
    【解决方案4】:

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

    【讨论】:

    • 一般来说,这是错误的。有些操作系统提供了公平的互斥锁,并且在这些操作系统中,OP 的场景将按预期工作。
    【解决方案5】:

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

    如果你想解决这个问题,你总是可以让出当前线程。您可以使用 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 的角度来看仍然可以正常工作)会带来相当大的性能提升

    【讨论】:

      【解决方案6】:

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

      谢天谢地,有一些开源实现,例如 yamc(仅标头库)。

      【讨论】:

        【解决方案7】:

        你怎么知道这个:

        当线程 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?
        

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

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

        【讨论】:

        • 仅供参考,如果线程执行的工作不同,那么肯定是微不足道的。例如;线程 A 将 x 乘以 2,线程 B 将 x 增加 3。操作顺序将产生唯一的结果。
        • @alfalfasprout 当然,事后你可以知道哪个先发生。问题是,您如何判断线程是否当前在等待互斥体,而线程在等待互斥体之前是否刚刚被调度程序挂起一条指令。你无法分辨(除非你在 OS/kernel/scheduler 中)。
        猜你喜欢
        • 2012-06-13
        • 1970-01-01
        • 1970-01-01
        • 2012-12-25
        • 1970-01-01
        • 1970-01-01
        • 2017-07-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多