无锁袋算法的最优内存排序

时间:2015-10-15 01:31:57

标签: c++11 atomic lock-free

以下用于无锁包的C ++ 11编码算法中NQFENCE和DQFENCE内存屏障的最佳设置是什么?

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

令人惊讶的是(至少对我来说!)它似乎几乎是最佳的(一般而言)(无论这意味着什么)。它还有一些非常讨厌的属性,例如写入程序线程无法无限地排队数据的可能性,以及除了一个写入程序线程在不同CPU上运行的可能性无限期地停止这些CPU。然而,该属性看似普遍(对于所有可能的实现都是如此)。同上读者。

说明:dequeue操作从插槽0开始,并在那里交换NULL指针。如果结果为非NULL,则返回它,在那里粘贴了NULL。否则我们将NULL换成NULL,所以我们递增槽索引并在睡眠后重试。

排队操作也从插槽零开始,并交换数据以存储其中的值。如果结果为NULL,我们已完成工作并返回。否则,我们错误地替换了一些其他指针,所以我们递增索引,稍微睡一会,然后尝试将该值放回队列中继续。

我们不会跟踪头部或尾部位置,因为这会在无竞争操作中需要额外的约束和损害性能。当存在争用时,可能需要额外的时间来搜索阵列以寻找合适的时隙,但这可能是合乎需要的!

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

这个算法很有意思,因为与其他复杂算法不同,每个入队和出队方法只需要一个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;
  }
};

1 个答案:

答案 0 :(得分:2)

如果外部代码(例如value)“按原样”使用存储在包中的printf("Value: %p", value);,则不需要内存顺序约束;同时NQFENCEDQFENCE可能只是::std::memory_order_relaxed

否则,(例如value是指向struct / object的指针,哪些字段有一个),NQFENCE应该是::std::memory_order_release,以确保在发布之前初始化对象的字段物体。至于DQFENCE,在简单的object-with-fields情况下它可以是::std::memory_order_consume,因此每个值的字段将在之后获取值本身。在常见情况下::std::memory_order_acquire应该用于DQFENCE。因此,消费者可以看到生产者在发布价值之前执行的每个记忆操作。

在讨论性能时,只需NQFENCEenqueue()的第一次迭代 就可以了,其他迭代可以安全地使用::std::memory_order_relaxed:< / p>

 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。由于最后一次迭代只能在原子操作之后检测到,因此对于这种情况没有通用优化。您可以使用其他 fence 而不是内存顺序:

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;
  }

如果atomic_exchange_explicit在宽松的顺序下实际上更快,这将获得性能,但如果此操作已经暗示顺序排序(参见Anton的comment),则会失去性能。