C++11 无锁包算法的最优内存排序

C++11 无锁包算法的最优内存排序,c++11,atomic,lock-free,C++11,Atomic,Lock Free,在以下无锁行李的C++11编码算法中,NQFENCE和DQFENCE内存屏障的最佳设置是什么 描述:此算法是一个(否则)接近最优的多生产者多消费者无锁队列,用于向线程池提供非空指针。它的正确性非常明显(模错误!)。它是不可线性化的,也就是说,由单个线程排队的数据可能会无序退出队列,因此它不适用于单个使用者队列(或使用者不独立的环境) 令人惊讶的是(至少对我来说!)它看起来几乎是最优的(总体而言)(无论这意味着什么)。它还有一些非常糟糕的特性,例如写入线程可能无法无限期地将数据排队,以及除了一个写

在以下无锁行李的C++11编码算法中,NQFENCE和DQFENCE内存屏障的最佳设置是什么

描述:此算法是一个(否则)接近最优的多生产者多消费者无锁队列,用于向线程池提供非空指针。它的正确性非常明显(模错误!)。它是不可线性化的,也就是说,由单个线程排队的数据可能会无序退出队列,因此它不适用于单个使用者队列(或使用者不独立的环境)

令人惊讶的是(至少对我来说!)它看起来几乎是最优的(总体而言)(无论这意味着什么)。它还有一些非常糟糕的特性,例如写入线程可能无法无限期地将数据排队,以及除了一个写入线程之外的所有写入线程都可能在不同的CPU上运行,从而无限期地暂停这些CPU。然而,该属性似乎是通用的(对于所有可能的实现都是如此)。读者也一样

说明:出列操作从插槽0开始,并用空指针交换该插槽中的值。如果结果为非NULL,则返回它,并在那里粘贴NULL。否则,我们将NULL替换为NULL,因此我们增加插槽索引,并在睡眠后重试

排队操作也从插槽0开始,并将数据交换到存储位置,使其具有该位置的值。如果结果为空,则表示我们已完成工作并返回。否则,我们错误地替换了其他指针,所以我们增加索引,稍微休眠,然后继续尝试将该值放回队列

我们不跟踪头部或尾部位置,因为这将需要额外的约束,并损害非竞争性运营中的绩效。当存在争用时,可能需要额外的时间在阵列中搜索合适的插槽,但是这可能是可取的

我们可以使用近似的头跟踪和尾跟踪:这将涉及到索引位置的原子读写,并具有宽松(即,无)的内存顺序。原子性仅用于确保写入整个值。这些指标可能不准确,但这不会影响算法的正确性。然而,并不完全清楚这种修改实际上会提高性能

该算法很有趣,因为与其他复杂算法不同,每个入队和出队方法只需要一个CAS操作

#include <atomic>
#include <stdlib.h>

#define NQFENCE ::std::memory_order_cst
#define DQFENCE ::std::memory_order_cst

struct bag {
  ::std::atomic <void *> volatile *a;
  size_t n;

  bag (size_t n_) : n (n_), 
    a((::std::atomic<void*>*)calloc (n_ , sizeof (void*))) 
  {}

  void enqueue(void *d) 
  { 
    size_t i = 0;
    while 
    (
      (d = ::std::atomic_exchange_explicit(a + i, d, 
        NQFENCE))
    ) 
    { 
      i = (i + 1) % n; 
      SLEEP;
    }
  }
  void *dequeue () 
  { 
    size_t i = 0;
    void *d = nullptr;
    while 
    (
      !(d = ::std::atomic_exchange_explicit(a + i, d, 
        DQFENCE))
    ) 
    { 
      i = (i + 1) % n; 
      SLEEP;
    }
    return d;
  }
};
#包括
#包括
#定义NQFENCE::std::内存\u顺序\u cst
#定义DQFENCE::std::内存\u顺序\u cst
结构袋{
::std::原子挥发性*a;
尺寸;
袋子(尺寸):n(n),
a((::std::atomic*)calloc(n_,sizeof(void*))
{}
无效排队(无效*d)
{ 
尺寸i=0;
虽然
(
(d=::std::原子交换显式(a+i,d,
NQFENCE)
) 
{ 
i=(i+1)%n;
睡觉
}
}
void*出列()
{ 
尺寸i=0;
void*d=nullptr;
虽然
(
!(d=::std::原子交换显式(a+i,d,
DQFENCE)
) 
{ 
i=(i+1)%n;
睡觉
}
返回d;
}
};

如果外部代码(例如,
printf(“值:%p”,值);
)按“原样”使用存储在袋子中的
),则不需要内存顺序约束;也就是说,
NQFENCE
DQFENCE
可能只是
:std::memory\u order\u released

否则,(例如,
value
是指向结构/对象的指针,哪些字段有意义),
NQFENCE
应该是
::std::memory\u order\u release
,以确保在发布对象之前初始化对象的字段。至于
DQFENCE
,它可以是
::std::memory\u order\u consumer
,在带有字段的简单对象中,因此每个值的字段都将在值本身之后提取。在常见情况下,
::std::memory\u order\u acquire
应用于
DQFENCE
。所以,生产者在发布值之前执行的每个内存操作都会被消费者看到

当谈到性能时,仅在
enqueue()中的第一次迭代中使用
NQFENCE
就足够了,其他迭代可以安全地使用
::std::memory\u order\u relaxed

 void enqueue(void *d) 
  { 
    size_t i = 0;
    if(d = ::std::atomic_exchange_explicit(a + i, d, 
        NQFENCE))
    {
      do
      {
        i = (i + 1) % n; 
        SLEEP;
      } while(d = ::std::atomic_exchange_explicit(a + i, d, 
        ::std::memory_order_relaxed));
    }
  }
类似地,只有
dequeue()
中的最后一次迭代需要
DQFENCE
。由于最后一次迭代只能在原子操作后检测到,所以这种情况下没有通用的优化。您可以使用附加围栏代替内存顺序:

void *dequeue () 
  { 
    size_t i = 0;
    void *d = nullptr;
    while 
    (
      !(d = ::std::atomic_exchange_explicit(a + i, d, 
        ::std::memory_order_relaxed))
    ) 
    { 
      i = (i + 1) % n; 
      SLEEP;
    }

    ::std::atomic_thread_fence(DQFENCE);
    return d;
  }

如果
原子交换显式
在松弛顺序下实际上更快,则此操作将获得性能,但如果此操作已经暗示顺序排序(请参见Anton的),则此操作将失去性能。

此问题仅适用于非英特尔硬件,因为在英特尔硬件上,CAS具有
memory\u order\u cst
语义。我正在构建一个独立于平台的产品,所以它很重要。例如,它可能(有一天!)在手臂上跑步。好吧,这是有道理的,但我还是有点不安。围栏不在那里,因此客户端线程“可以看到对象的其余部分”,事实上,我甚至没有想到这一点,应该这样做。我担心的是,制作人在袋子里放了一个其他制作人和消费者都能看到的条目,否则算法就无法工作。类似地,当消费者退出队列时,会放置一个NULL来删除元素,并且必须看到该元素。例如,对于宽松的排序,为什么两个空值的CA不能同时获取相同的非空值?[注释太短:]因此,如果Thr1不执行CAS addr,NULL并获取V not NULL,为什么Thr2不执行CAS addr,NULL并获取V not NULL?原子性确保线程和写入整个值,并且两个CA必须序列化,但如果内存顺序放松,什么确保CAS#1写入的NULL将由CAS#2返回?这是任何CAS操作的特征,任何两个CAS操作都不能读取相同的值。C++标准的第27.3.12段说: