并发修改 double[][] 元素而不加锁
Concurrent modification of double[][] elements without locking
我有一个锯齿状的 double[][]
数组,可以由多个线程同时修改。我想让它成为线程安全的,但如果可能的话,没有锁。线程很可能以数组中的相同元素为目标,这就是出现整个问题的原因。我找到了使用 Interlocked.CompareExchange
方法自动增加双精度值的代码:Why is there no overload of Interlocked.Add that accepts Doubles as parameters?
我的问题是:如果 Interlocked.CompareExchange
中有参差不齐的数组引用,它会保持原子性吗?非常感谢您的见解。
举个例子:
public class Example
{
double[][] items;
public void AddToItem(int i, int j, double addendum)
{
double newCurrentValue = items[i][j];
double currentValue;
double newValue;
SpinWait spin = new SpinWait();
while (true) {
currentValue = newCurrentValue;
newValue = currentValue + addendum;
// This is the step of which I am uncertain:
newCurrentValue = Interlocked.CompareExchange(ref items[i][j], newValue, currentValue);
if (newCurrentValue == currentValue) break;
spin.SpinOnce();
}
}
}
是的,它仍然是原子的并且 thread-safe。对同一单元格的任何调用都将传递相同的 address-to-a-double。诸如它是否在对象的字段数组中的详细信息是无关紧要的。
但是,行:
double newCurrentValue = items[i][j];
不是原子的 - 理论上 可以给出一个撕裂的值(尤其是在 x86 上)。在这种情况下,这实际上没问题,因为在 torn-value 场景中,它只会进入循环,算作碰撞,然后重做 - 这次使用 CompareExchange
.[=12 中的 known-atomic 值=]
您似乎希望最终向数组项添加一些值。
我想只有值的更新(数组本身保持同一块内存)并且所有更新都是通过这个 AddToItem
方法完成的。
所以,你每次都必须读取更新的值(否则你会丢失其他线程所做的更改,或者得到无限循环)。
public class Example
{
double[][] items;
public void AddToItem(int i, int j, double addendum)
{
var spin = new SpinWait();
while (true)
{
var valueAtStart = Volatile.Read(ref items[i][j]);
var newValue = valueAtStart + addendum;
var oldValue = Interlocked.CompareExchange(ref items[i][j], newValue, valueAtStart);
if (oldValue.Equals(valueAtStart))
break;
spin.SpinOnce();
}
}
}
请注意,我们必须使用一些 volatile 方法来读取 items[i][j]
。 Volatile.Read
用于避免 .NET 内存模型允许的一些不必要的优化(请参阅 ECMA-334 和 ECMA-335 规范)。
由于我们以原子方式更新值(通过 Interlocked.CompareExchange
),因此通过 Volatile.Read
.
读取 items[i][j]
就足够了
如果不是所有对该数组的更改都在此方法中完成,那么最好编写一个循环,在其中创建数组的本地副本,修改它并更新对新数组的引用(使用 Volatile.Read
和 Interlocked.CompareExchange
)
我有一个锯齿状的 double[][]
数组,可以由多个线程同时修改。我想让它成为线程安全的,但如果可能的话,没有锁。线程很可能以数组中的相同元素为目标,这就是出现整个问题的原因。我找到了使用 Interlocked.CompareExchange
方法自动增加双精度值的代码:Why is there no overload of Interlocked.Add that accepts Doubles as parameters?
我的问题是:如果 Interlocked.CompareExchange
中有参差不齐的数组引用,它会保持原子性吗?非常感谢您的见解。
举个例子:
public class Example
{
double[][] items;
public void AddToItem(int i, int j, double addendum)
{
double newCurrentValue = items[i][j];
double currentValue;
double newValue;
SpinWait spin = new SpinWait();
while (true) {
currentValue = newCurrentValue;
newValue = currentValue + addendum;
// This is the step of which I am uncertain:
newCurrentValue = Interlocked.CompareExchange(ref items[i][j], newValue, currentValue);
if (newCurrentValue == currentValue) break;
spin.SpinOnce();
}
}
}
是的,它仍然是原子的并且 thread-safe。对同一单元格的任何调用都将传递相同的 address-to-a-double。诸如它是否在对象的字段数组中的详细信息是无关紧要的。
但是,行:
double newCurrentValue = items[i][j];
不是原子的 - 理论上 可以给出一个撕裂的值(尤其是在 x86 上)。在这种情况下,这实际上没问题,因为在 torn-value 场景中,它只会进入循环,算作碰撞,然后重做 - 这次使用 CompareExchange
.[=12 中的 known-atomic 值=]
您似乎希望最终向数组项添加一些值。
我想只有值的更新(数组本身保持同一块内存)并且所有更新都是通过这个 AddToItem
方法完成的。
所以,你每次都必须读取更新的值(否则你会丢失其他线程所做的更改,或者得到无限循环)。
public class Example
{
double[][] items;
public void AddToItem(int i, int j, double addendum)
{
var spin = new SpinWait();
while (true)
{
var valueAtStart = Volatile.Read(ref items[i][j]);
var newValue = valueAtStart + addendum;
var oldValue = Interlocked.CompareExchange(ref items[i][j], newValue, valueAtStart);
if (oldValue.Equals(valueAtStart))
break;
spin.SpinOnce();
}
}
}
请注意,我们必须使用一些 volatile 方法来读取 items[i][j]
。 Volatile.Read
用于避免 .NET 内存模型允许的一些不必要的优化(请参阅 ECMA-334 和 ECMA-335 规范)。
由于我们以原子方式更新值(通过 Interlocked.CompareExchange
),因此通过 Volatile.Read
.
读取 items[i][j]
就足够了
如果不是所有对该数组的更改都在此方法中完成,那么最好编写一个循环,在其中创建数组的本地副本,修改它并更新对新数组的引用(使用 Volatile.Read
和 Interlocked.CompareExchange
)