C++ 原子:如何只允许一个线程访问一个函数?
C++ atomics: how to allow only a single thread to access a function?
我想编写一个一次只能由一个线程访问的函数。我不需要忙等待,如果另一个线程已经 运行 了,那么残酷的 'rejection' 就足够了。这是我到目前为止想出的:
std::atomic<bool> busy (false);
bool func()
{
if (m_busy.exchange(true) == true)
return false;
// ... do stuff ...
m_busy.exchange(false);
return true;
}
- 原子交换的逻辑是否正确?
- 将两个原子操作标记为
std::memory_order_acq_rel
是否正确?据我了解,宽松的排序 (std::memory_order_relaxed
) 不足以防止重新排序。
您的原子交换实现可能有效。但是尝试在没有锁的情况下进行线程安全编程总是充满问题并且通常更难维护。
除非需要提高性能,否则 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;
}
你的代码在我看来是正确的,只要你通过失败离开临界区,而不是返回或抛出异常。
您可以通过 release
商店解锁; RMW(如交换)是不必要的。初始兑换只需要acquire
。 (但确实需要像 exchange
或 compare_exchange_strong
这样的原子 RMW)
请注意,ISO C++ 表示获取 std::mutex
是一个“获取”操作,而释放是一个“释放”操作,因为这是保持临界区包含在获取和获取之间所需的最低限度释放。
您的算法与自旋锁完全一样,但如果锁已被占用则无需重试。 (即只是一个 try_lock)。关于锁定的必要内存顺序的所有推理也适用于此。 您实现的内容在逻辑上等同于@selbie 的回答中的 try_lock
/ unlock
, 并且很可能在性能上也是等同的。如果您从不使用 mtx.lock()
或其他任何东西,那么您永远不会真正阻塞,即等待另一个线程做某事,因此您的代码在进度保证意义上仍然可能是 lock-free。
用 atomic<bool>
滚动你自己的可能很好;在这里使用 std::mutex
对你没有任何好处;您希望 它仅针对尝试锁定和解锁执行此操作。这当然是可能的(有一些额外的函数调用开销),但一些实现可能会做更多的事情。您没有使用除此之外的任何功能。 std::mutex
给你的一件好事是知道它安全正确地实现了 try_lock
和 unlock
。但是如果你了解锁定和获取/释放,那么你自己就很容易做到这一点。
不滚动自己的锁定的通常性能原因是 mutex
将针对 OS 和典型的硬件进行调整,其中包括指数退避、x86 pause
指令,而旋转几次,然后回退到系统调用。并通过 Linux futex
等系统调用进行高效唤醒。所有这些只对阻塞行为有利。 .try_lock
将其全部闲置,如果您从未有任何线程处于休眠状态,那么 unlock
永远不会有任何其他线程需要通知。
使用 std::mutex
有一个好处:您可以使用 RAII 而无需滚动自己的包装器 class。 std::unique_lock
与 std::try_to_lock
政策将执行此操作。这将使您的函数异常安全,确保在退出前始终解锁,如果它获得了锁。
我想编写一个一次只能由一个线程访问的函数。我不需要忙等待,如果另一个线程已经 运行 了,那么残酷的 'rejection' 就足够了。这是我到目前为止想出的:
std::atomic<bool> busy (false);
bool func()
{
if (m_busy.exchange(true) == true)
return false;
// ... do stuff ...
m_busy.exchange(false);
return true;
}
- 原子交换的逻辑是否正确?
- 将两个原子操作标记为
std::memory_order_acq_rel
是否正确?据我了解,宽松的排序 (std::memory_order_relaxed
) 不足以防止重新排序。
您的原子交换实现可能有效。但是尝试在没有锁的情况下进行线程安全编程总是充满问题并且通常更难维护。
除非需要提高性能,否则 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;
}
你的代码在我看来是正确的,只要你通过失败离开临界区,而不是返回或抛出异常。
您可以通过 release
商店解锁; RMW(如交换)是不必要的。初始兑换只需要acquire
。 (但确实需要像 exchange
或 compare_exchange_strong
这样的原子 RMW)
请注意,ISO C++ 表示获取 std::mutex
是一个“获取”操作,而释放是一个“释放”操作,因为这是保持临界区包含在获取和获取之间所需的最低限度释放。
您的算法与自旋锁完全一样,但如果锁已被占用则无需重试。 (即只是一个 try_lock)。关于锁定的必要内存顺序的所有推理也适用于此。 您实现的内容在逻辑上等同于@selbie 的回答中的 try_lock
/ unlock
, 并且很可能在性能上也是等同的。如果您从不使用 mtx.lock()
或其他任何东西,那么您永远不会真正阻塞,即等待另一个线程做某事,因此您的代码在进度保证意义上仍然可能是 lock-free。
用 atomic<bool>
滚动你自己的可能很好;在这里使用 std::mutex
对你没有任何好处;您希望 它仅针对尝试锁定和解锁执行此操作。这当然是可能的(有一些额外的函数调用开销),但一些实现可能会做更多的事情。您没有使用除此之外的任何功能。 std::mutex
给你的一件好事是知道它安全正确地实现了 try_lock
和 unlock
。但是如果你了解锁定和获取/释放,那么你自己就很容易做到这一点。
不滚动自己的锁定的通常性能原因是 mutex
将针对 OS 和典型的硬件进行调整,其中包括指数退避、x86 pause
指令,而旋转几次,然后回退到系统调用。并通过 Linux futex
等系统调用进行高效唤醒。所有这些只对阻塞行为有利。 .try_lock
将其全部闲置,如果您从未有任何线程处于休眠状态,那么 unlock
永远不会有任何其他线程需要通知。
使用 std::mutex
有一个好处:您可以使用 RAII 而无需滚动自己的包装器 class。 std::unique_lock
与 std::try_to_lock
政策将执行此操作。这将使您的函数异常安全,确保在退出前始终解锁,如果它获得了锁。