最好用一些简单的代码来解释这个问题。
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()
无锁(您可以将data
和max
视为atomic
)?
注意:这是原始帖子的简单版本,与树结构无关。
答案 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;改变了。然而它有 - 从0xA000
到0xB000
(当你不看时)回到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。
这并不简单。因此,如果有一个正确的算法(并且我认为存在,在某些约束内),则不太可能没有错误。也就是说,实际上可能存在一个正确的算法,但是你找不到它 - 你找到的是一种看似正确的算法。