C++ 原子:如何只允许一个线程访问一个函数?

Ign*_*ant 7 c++ multithreading lock-free mutual-exclusion stdatomic

我想编写一个一次只能由一个线程访问的函数。我不需要忙碌的等待,如果另一个线程已经在运行它,那么残酷的“拒绝”就足够了。这是我到目前为止想出的:

std::atomic<bool> busy (false);

bool func()
{
    if (m_busy.exchange(true) == true)
        return false;  

    // ... do stuff ...

    m_busy.exchange(false);
    return true;
}
Run Code Online (Sandbox Code Playgroud)
  1. 原子交换的逻辑是否正确?
  2. 将两个原子操作标记为 是否正确std::memory_order_acq_rel?据我了解,宽松的排序 ( std::memory_order_relaxed) 不足以防止重新排序。

sel*_*bie 7

您的原子交换实现可能有效。但是尝试在没有锁的情况下进行线程安全编程总是充满问题并且通常更难维护。

除非需要提高性能,否则您只需要std::mutex使用该try_lock()方法,例如:

std::mutex mtx;

bool func()
{
    // making use of std::unique_lock so if the code throws an
    // exception, the std::mutex will still get unlocked correctly...

    std::unique_lock<std::mutex> lck(mtx, std::try_to_lock);
    bool gotLock = lck.owns_lock();

    if (gotLock)
    {
        // do stuff
    }

    return gotLock;
}
Run Code Online (Sandbox Code Playgroud)

  • @Ignorant:您的代码在逻辑上与此等效,并且很可能在性能上等效。如果你从不使用`mtx.lock()`或其他什么,你实际上永远不会阻塞,即等待锁,所以你的代码仍然可能是[无锁](https://en.wikipedia.org/wiki/进度保证意义上的非阻塞算法)。“无锁”并不意味着“避免任何名称中带有“锁”的东西”。使用 std::atomic 的手卷自旋锁不会是无锁的,而仅使用 std::mutex *的非阻塞功能*仍然是无锁的(并且可能是无等待的)。 (3认同)

Pet*_*des 2

你的代码对我来说看起来是正确的,只要你通过退出而离开关键部分,不返回或抛出异常。

您可以通过商店解锁release;RMW(如交换)是不必要的。最初的兑换只需要acquire. (但确实需要是像exchangeor一样的原子 RMW compare_exchange_strong

请注意,ISO C++ 表示获取 astd::mutex是“获取”操作,而释放 a 是“释放”操作,因为这是保持获取和释放之间包含的临界区所需的最低限度。


您的算法与自旋锁完全相同,但如果锁已被占用,则无需重试。(即只是一个 try_lock)。所有关于锁定所需内存顺序的推理也适用于此。 您所实现的内容在逻辑上等同于@selbie 答案中的try_lock/unlock ,并且很可能在性能上等同。如果您从不使用mtx.lock()或其他什么,您实际上永远不会阻塞,即等待另一个线程执行某些操作,因此您的代码在进度保证意义上仍然可能是无锁的。

自己动手atomic<bool>可能会很好;使用std::mutex这里不会给你带来任何好处;您希望它只为尝试锁定和解锁执行此操作。这当然是可能的(有一些额外的函数调用开销),但某些实现可能会做更多的事情。您没有使用除此之外的任何功能。给您带来的一件好事std::mutex是知道它安全且正确地实现了try_lockunlock。但如果您了解锁定和获取/释放,那么您自己就很容易做到这一点。

不滚动自己的锁定的通常性能原因是,mutex将针对操作系统和典型硬件进行调整,使用指数退避、pause旋转几次的 x86 指令等内容,然后回退到系统调用。并通过像Linux这样的系统调用进行高效唤醒futex。所有这些都只对阻塞行为有利。 .try_lock使所有线程都未使用,并且如果您从未有任何线程处于休眠状态,则unlock永远不会有任何其他线程需要通知。


使用 RAII 有一个优点std::mutex:您可以使用 RAII,而无需推出自己的包装类。std::unique_lock有了std::try_to_lock政策就会做到这一点。这将使您的函数异常安全,确保在退出之前始终解锁(如果它获得了锁)。