std::atomic 等待操作如何工作?

Afs*_*hin 13 c++ c++20

从 C++20 开始,std::atomicwait()and notify_one()/notify_all()操作。但我不明白它们到底是如何工作的。cppreference 说

执行原子等待操作。其行为就像重复执行以下步骤:

  • 将 this->load(order) 的值表示与旧的值表示进行比较。
    • 如果它们相等,则阻塞直到*this收到notify_one()或notify_all()通知,或者线程被虚假地解除阻塞。
    • 否则,返回。

仅当值发生更改时,这些函数才保证返回,即使底层实现虚假地解除阻塞。

我不太明白这两个部分是如何相互关联的。这是否意味着如果值没有改变,那么即使我使用notify_one()/notify_all()方法,该函数也不会返回?这意味着该操作在某种程度上等于以下伪代码?

while (*this == val) {
    // block thread
}
Run Code Online (Sandbox Code Playgroud)

Hom*_*512 8

是的,就是这样。notify_one/all 只是为等待线程提供检查值更改的机会。如果它保持不变,例如因为不同的线程已将该值设置回其原始值,则该线程将保持阻塞状态。

注意:此代码的有效实现是使用互斥体和条件变量的全局数组。然后原子变量通过哈希函数通过它们的指针映射到这些对象。这就是为什么你会得到虚假唤醒。一些原子共享相同的条件变量。

像这样的东西:


std::mutex atomic_mutexes[64];
std::condition_variable atomic_conds[64];

template<class T>
std::size_t index_for_atomic(std::atomic<T>* ptr) noexcept
{ return reinterpret_cast<std::size_t>(ptr) / sizeof(T) % 64; }

void atomic<T>::wait(T value, std::memory_order order)
{
    if(this->load(order) != value)
        return;
    std::size_t index = index_for_atomic(this);
    std::unique_lock<std::mutex> lock(atomic_mutexes[index]);
    while(this->load(std::memory_order_relaxed) == value)
        atomic_conds[index].wait(lock);
}
template<class T>
void std::atomic_notify_one(std::atomic<T>* ptr)
{
    const std::size_t index = index_for_atomic(ptr);
    /*
     * normally we don't need to hold the mutex to notify
     * but in this case we updated the value without holding
     * the lock. Therefore without the mutex there would be
     * a race condition in wait() between the while-loop condition
     * and the loop body
     */
    std::lock_guard<std::mutex> lock(atomic_mutexes[index]);
    /*
     * needs to notify_all because we could have multiple waiters
     * in multiple atomics due to aliasing
     */
    atomic_conds[index].notify_all();
}
Run Code Online (Sandbox Code Playgroud)

真正的实现可能会使用操作系统原语,例如 Windows 上的 WaitForAddress 或 Linux 上的(至少对于 int 大小的类型)futex。

  • @macomphy 我只是在这里展示一个例子。真正的实现是可以自由地做任何它想做的事。但是,如果您查看 libatomic(GCC 将其用于需要互斥锁的原子),它们还使用 64 个互斥锁,并在它们之间添加 64 字节大小(缓存行大小)的填充。它使用 1 个 4kiB 内存页。一般来说,您希望同时运行的线程数量至少多于最大数量的 2 倍,以降低哈希冲突的风险。 (2认同)