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_;
};
但是我发现push中的代码是无锁的而不是等待的。假设多个生产者正在调用推送,至少有一个生产者可以取得进展;其他生产者只是再次运行while循环,直到取得进展。存在一种使特定线程在不可预测的时间内匮乏的调度方式。
wait-free的定义告诉我们,任何提供时间片的给定线程都能够取得一些进展并最终完成,而无锁告诉我们至少有一个线程可以取得进展。所以上面的代码似乎满足了无锁的定义。
我的理解是否有错误?
答案 0 :(得分:0)
是的,对于抽象的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重试循环之间可能并没有太大差异,即使在x xchg
或swp
具有真正的硬件支持而无需运行的x86或其他ISA上重试循环。 但是最好将结果描述为无锁而不是无等待,因为即使在交换时,只有一个线程可以立即在修改head_
上取得进展。可能的等待时间与其他线程的数量(以及原子操作的公平性)成比例。
因此,当您查看为真实硬件编译的代码时,定义就会变得有些模糊。