根据intel博客实现concurrent_vector

时间:2017-10-16 01:48:27

标签: c++ arrays multithreading lockless

我正在尝试实现一个线程安全的无锁容器,类似于std :: vector,根据这个https://software.intel.com/en-us/blogs/2008/07/24/tbbconcurrent_vector-secrets-of-memory-organization

根据我的理解,为了防止重新分配并使所有线程上的所有迭代器无效,而不是单个连续数组,它们会添加新的连续块。
他们添加的每个块都具有增加2的幂的大小,因此他们可以使用log(index)来找到[index]处的项目所在的正确段。

从我收集的内容来看,他们有一个指向段的静态指针数组,因此他们可以快速访问它们,但是他们不知道用户想要多少段,所以他们创建了一个小的初始值,如果是如果段超过当前计数,则会分配一个巨大的段并切换到使用该段。

问题是,添加新段无法以无锁线程安全方式完成,或者至少我还没弄明白如何。我可以原子地增加当前大小,但仅限于此 而且从小型指针到大型段指针的切换涉及大量的分配和内存副本,所以我无法理解他们是如何做到的。

他们有一些在线发布的代码,但所有重要的功能都没有可用的源代码,它们都在他们的Thread Building Blocks DLL中。以下是一些演示此问题的代码:

template<typename T>
class concurrent_vector
{
    private:
        int size = 0;
        int lastSegmentIndex = 0;

        union
        {
            T* segmentsSmall[3];
            T** segmentsLarge;
        };

        void switch_to_large()
        {
            //Bunch of allocations, creates a T* segmentsLarge[32] basically and reassigns all old entries into it
        }

    public:
        concurrent_vector()
        {
            //The initial array is contiguous just for the sake of cache optimization
            T* initialContiguousBlock = new T[2 + 4 + 8]; //2^1 + 2^2 + 2^3
            segmentsSmall[0] = initialContiguousBlock;
            segmentsSmall[1] = initialContiguousBlock + 2;
            segmentsSmall[2] = initialContiguousBlock + 2 + 4;
        }

        void push_back(T& item)
        {
            if(size > 2 + 4 + 8)
            {
                switch_to_large(); //This is the problem part, there is no possible way to make this thread-safe without a mutex lock. I don't understand how Intel does it. It includes a bunch of allocations and memory copies.
            }

            InterlockedIncrement(&size); //Ok, so size is atomically increased

            //afterwards adds the item to the appropriate slot in the appropriate segment
        }
};

1 个答案:

答案 0 :(得分:2)

我不会尝试将segmentsLargesegmentsSmall作为联盟。是的,这又浪费了一个指针。然后指针,让我们调用它segments最初可以指向segmentsSmall。

另一方面,其他方法总是可以使用相同的指针,这使得它们更简单。

可以通过指针的一次比较交换来实现从小到大的切换。

我不确定如何通过工会安全地实现这一目标。

这个想法看起来像这样(请注意,我使用的是英特尔库早于C ++ 11,因此他们很可能使用原子内在函数)。 这可能会遗漏一些细节,我相信英特尔人已经考虑了更多,所以你可能不得不对所有其他方法的实现进行检查。

#include <atomic>
#include <array>
#include <cstddef>
#include <climits>

template<typename T>
class concurrent_vector
{
private:
  std::atomic<size_t> size;
  std::atomic<T**> segments;
  std::array<T*, 3> segmentsSmall;
  unsigned lastSegmentIndex = 0;

  void switch_to_large()
  {
    T** segmentsOld = segments;
    if( segmentsOld == segmentsSmall.data()) {
      // not yet switched
      T** segmentsLarge = new T*[sizeof(size_t) * CHAR_BIT];
      // note that we leave the original segment allocations alone and just copy the pointers
      std::copy(segmentsSmall.begin(), segmentsSmall.end(), segmentsLarge);
      for(unsigned i = segmentsSmall.size(); i < numSegments; ++i) {
        segmentsLarge = nullptr;
      }
      // now both the old and the new segments array are valid
      if( segments.compare_exchange_strong(segmentsOld, segmentsLarge)) {
        // success!
        return;
      }  else {
        // already switched, just clean up
        delete[] segmentsLarge;
      }
    }
  }

public:
  concurrent_vector()  : size(0), segments(segmentsSmall.data())
  {
    //The initial array is contiguous just for the sake of cache optimization
    T* initialContiguousBlock = new T[2 + 4 + 8]; //2^1 + 2^2 + 2^3
    segmentsSmall[0] = initialContiguousBlock;
    segmentsSmall[1] = initialContiguousBlock + 2;
    segmentsSmall[2] = initialContiguousBlock + 2 + 4;
  }

  void push_back(T& item)
  {
    if(size > 2 + 4 + 8) {
      switch_to_large();
    }
    // here we may have to allocate more segments atomically
    ++size;

    //afterwards adds the item to the appropriate slot in the appropriate segment
  }
};
相关问题