如何在一定范围内将无锁更新索引设置为最大值?

时间:2017-07-05 15:24:58

标签: c++ multithreading atomic lock-free

最好用一些简单的代码来解释这个问题。

struct foo
{
    static constexpr auto N=8;
    double data[N];            //  initialised at construction
    int max;                   //  index to maximum: data[max] is largest value

    // if value < data[index]:
    //   – update data[index] = value
    //   - update max
    void update(int index, double value)
    {
        if(value >= data[index])
            return;
        data[index] = value;
        if(index==max)         // max unaffected if index!=max
            for(index=0; index!=N; ++index)
                if(data[index] > data[max])
                    max = index;
    }
};

现在,我想让foo::update()线程安全,即允许来自不同线程的并发调用,其中参与的线程不能使用相同的index调用。一种方法是向foo添加互斥锁或简单的自旋锁(争用可以假定为低):

struct foo
{
    static constexpr auto N=8;
    std::atomic_flag lock = ATOMIC_FLAG_INIT;
    double data[N];
    int max;

    // index is unique to each thread
    // if value < data[index]:
    //   – update data[index] = value
    //   - update max
    void update(int index, double value)
    {
        if(value >= data[index])
            return;
        while(lock.test_and_set(std::memory_order_acquire));  // aquire spinlock
          data[index] = value;
          if(index==max)
              for(index=0; index!=N; ++index)
                  if(data[index] > data[max])
                      max = index;
        lock.clear(std::memory_order_release);                // release spinlock
    }
};

但是,如何实施foo::update() 无锁(您可以将datamax视为atomic)?

注意:这是原始帖子的简单版本,与树结构无关。

1 个答案:

答案 0 :(得分:1)

所以,IIUC,如果阵列低于已经存在的值,那么阵列只会获得新值(并且我不会担心初始值是如何到达那里的),如果当前最大值降低,找到一个新的最大。

其中一些并不太难。 但有些是......更难。

所以&#34; if值&lt; data [index]然后写入数据&#34;需要在CAS循环中。类似的东西:

auto oldval = data[index].load(memory_order_relaxed);
do
   if (value <= oldval) return;
while ( ! data[index].compare_exchange_weak(oldval, value) );
// (note that oldval is updated to data[index] each time comp-exch fails)

所以现在data [index]有了新的较低值。真棒。而且比较容易。 现在关于最大

第一个问题 - 最大错误是否可以?因为它可能目前是错误的(在我们的场景中,我们在处理max之前更新数据[index]。)

在某些方面它可能是错的,而不是其他方面吗?即我们说我们的数据只是两个条目:

data[2] = { 3, 7 };

我们希望update(1, 2)即将7更改为2。 (并因此更新最大!)

场景A:首先设置数据,然后设置max:

data[1] = 2;
pause(); // ie scheduler pauses this thread
max = 0; // data[0]==3 is now max

如果另一个帖子在pause()进来,则data[max]错误:2而不是3: - (

场景B:先设置最大值:

max = 0; // it will be "shortly"?
pause();
data[1] = 2;

现在一个线程可以将数据[max]读取为3 ,而7仍然在数据中。但是7很快就会成为2&#34;那么可以吗?是不是错了&#34;情景A?取决于使用情况? (即如果重要的是&#34;这是max&#34;我们就是这样。但如果max是唯一重要的东西,为什么要存储所有数据呢?)

要问&#34;错误确定&#34;似乎很奇怪,但在某些无锁情况下,它实际上是一个有效的问题。对我而言,B有机会对某些用途感到满意,而A则没有。

此外,这很重要:

数据[max]总是错误的,即使是在完美的算法中也是如此

我的意思是你需要意识到数据[max],一读到它已经过时了 - 如果你生活在一个无锁的世界里。因为它一读到就可能已经改变了。 (另外因为数据和最大值是独立变化的。但即使你有一个函数getMaxValue(),它一旦返回它就会过时。)

可以吗?因为,如果没有,你显然需要锁定。但如果没问题,我们可以利用它 - 我们可能会返回一个我们知道有些不正确/过时的答案,但不会比你从外面看到的更不正确。

如果两种情况都不正常,那么必须同时更新max和data [index]。这很难,因为它们不适合无锁大小的块。

所以你可以添加一个间接层:

struct DataAndMax { double data[N]; int max; };
DataAndMax * ptr;

每当你需要更新max时,你需要创建一个全新的DataAndMax结构(即分配一个新的结构),以某种方式将它完全填满,然后以原子方式将ptr交换到新结构。 如果其他一些线程在您准备新数据时更改了ptr,那么您需要重新开始,因为您需要在数据中使用新数据。

如果ptr已经改变了两次,那么它可能看起来就像它没有改变一样,当它确实有:让我们说ptr目前有价值0xA000第二个线程在0xB000分配一个新的DataAndStruct,并将ptr设置为0xB000,并在0xA000释放旧的。现在还有另一个线程(第3个)进来,分配另一个DataAndStruct - 并且低并且看到分配器返回0xA000(为什么不,它刚被释放!)。所以这个第3个线程将ptr设置为0xA000

当你试图将ptr设置为0xC000时,这一切都会发生。所有你看到的是ptr 0xA000,后来仍然 0xA000,所以你认为它(和它的数据)没有&#39;改变了。然而它有 - 从0xA0000xB000(当你不看时)回到0xA000 - 地址是相同的,但数据是不同的。这被称为ABA问题。

现在,如果您知道最大线程数,则可以预先分配:

DataAndMax dataBufs[NUM_THREADS];
DataAndMax * ptr; // current DataAndMax

然后永远不会分配/删除并且永远不会有ABA问题。或者还有其他方法可以避免ABA。

让我们回过头来,想想我们将如何进行 - 无论如何 - 返回可能过时的最大值。我们可以使用它吗?

所以你进来,首先检查你要写的索引是否是重要的索引:

if (index != max) {
    // we are not touching max,
    // so nothing fancy here!
    data[index] value;
    return;
}
// else do it the hard way:
//...

但这已经错了。在设置之后和之前,max可能已经改变了。 每个集是否需要更新max!?!?

所以,如果N很小,你可以直接搜索获取最大值。如果有人在搜索时进行更新可能会出错,但请记住 - 如果有人在搜索后立即进行更新,或者在#34之后立即进行更新,那么也可能是错误的#34;因此,除了可能很慢之外,搜索与任何算法一样正确。你会发现一些最新的东西。 如果N == 8,我会使用搜索。当然。 你可以使用memory_order_relaxed搜索8个条目,这比使用更强的原子操作来维护任何东西要快。

我有其他想法:

更多记账?分别存储maxValue?

double data[N];
double maxValue;
int indexOfMax;


bool wasMax = false;
if (index == indexOfMax)
    wasMax = true;
data[index] = value; 
if (wasMax || index == indexOfMax)
    findMax(&indexOfMax, &maxValue); // linear search

那可能需要一个CAS循环。仍然是线性搜索,但可能不那么频繁?

也许每个条目都需要额外的数据?还不确定。

Hmmmm。

这并不简单。因此,如果有一个正确的算法(并且我认为存在,在某些约束内),则不太可能没有错误。也就是说,实际上可能存在一个正确的算法,但是你找不到它 - 你找到的是一种看似正确的算法。