为什么 Boost 原子使用中的多生产者队列是免等待的

Why multi-producer queue in Boost atomic usage is wait-free

本文关键字:生产者 队列 等待 Boost 为什么      更新时间:2023-10-16

Boost Atomic示例中的免等待多生产者队列:

template<typename T>
class waitfree_queue {
public:
struct node {
T data;
node * next;
};
void push(const T &data)
{
node * n = new node;
n->data = data;
node * stale_head = head_.load(boost::memory_order_relaxed);
do {
n->next = stale_head;
} while (!head_.compare_exchange_weak(stale_head, n, boost::memory_order_release));
}
node * pop_all(void)
{
T * last = pop_all_reverse(), * first = 0;
while(last) {
T * tmp = last;
last = last->next;
tmp->next = first;
first = tmp;
}
return first;
}
waitfree_queue() : head_(0) {}
// alternative interface if ordering is of no importance
node * pop_all_reverse(void)
{
return head_.exchange(0, boost::memory_order_consume);
}
private:
boost::atomic<node *> head_;
};

http://www.boost.org/doc/libs/1_63_0_b1/doc/html/atomic/usage_examples.html#boost_atomic.usage_examples.mp_queue

但我发现推送中的代码是无锁的,而不是无等待的。假设多个生产者正在调用推送,至少一个生产者可以取得进展;其他生产者只是再次运行 while 循环,直到取得进展。存在一种调度方式,可以使特定线程在不可预测的时间内匮乏。

无等待的定义告诉我们,任何提供时间片的给定线程都能够取得一些进展并最终完成,而无锁告诉我们至少有一个线程可以取得进展。所以上面的代码似乎满足无锁的定义。

我的理解有误吗?

是的,您的分析对于抽象C++模型看起来是正确的。

推送是无锁的,但不是无等待的。 CAS 重试循环head_,在我们尝试时,其他线程可以继续修改,因此任何给定线程都可以无限次重试。 所以它不是没有等待的。

而且,至少有一个线程会取得进展,并且线程可以休眠并阻塞所有其他线程,因此它是无锁的。


pop_all_reverse(因此pop_all)是免等待的。他们只进行无条件的原子交换(假设一些硬件公平性......)应该是无等待的。

如果在实际硬件上作为 LL/SC 重试循环实现,它也仅在技术上变为无锁,而不保证无等待。 但我认为硬件可以设计为通过有机会执行 LL 的核心来促进成功的 SC,以避免内核暂时获得处于独占状态的缓存行但在失去所有权之前无法完成其原子操作的可能性。 IDK 是否是典型的。 在最坏的情况下,我认为这甚至可以在没有线程取得进展的地方创建活锁。


更正常地说,交换总是在第一次执行时成功,但必须等待缓存行的所有权才能执行此操作。

CAS通常也是如此。我希望即使在高争用的情况下,实际重试也很少见。 通过循环的第一次行程中的 CAS 已经被解码并等待执行,只是等待第一次加载作为输入。 如果其他线程正在写入缓存行,则在到达之前该行将不可读,并且如果 CPU 注意到 CAS 正在等待并在发送常规读取请求后发送 RFO(读取所有权),则可能会以独占状态到达。

或者也许有些CPU不是那么聪明;如果线路到达共享状态,那么CAS将不得不等待对RFO的响应,这将为另一个内核提供一个大窗口来获取线路并在第一次加载和第一个CAS之间修改它。

但是在第一个 CAS 之后,加载结果来自以前的 CAS,所以它肯定会从核心拥有独占所有权的缓存行读取数据,另一个 CAS 可以立即运行并成功。

因此,在实践中,exchange与 CAS 重试循环之间可能没有太大区别,即使在 x86 或其他 ISA 上也是如此,在这些 ISA 上,xchgswp具有真正的硬件支持,无需重试循环即可运行。但结果可能比无等待更好地描述为无锁,因为即使使用 exchange,也只有一个线程可以同时在修改head_方面取得进展。 可能的等待时间与其他线程的数量(以及原子操作的公平性)成比例。

因此,当您查看为真实硬件编译的代码时,定义开始感觉有点模糊。

不过,我仍然不会将此队列描述为无需等待。 当然无锁