【问题标题】:Concurrent modification of double[][] elements without locking并发修改 double[][] 元素而不加锁
【发布时间】:2017-01-31 10:56:01
【问题描述】:

我有一个锯齿状的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();
            }
        }
    }

【问题讨论】:

  • 就我个人而言,我会将 newCurrentValue 重命名为 oldValue - 这只是令人困惑 :)
  • 您是否意识到 while() 与使用 SpinLock 基本相同,但增加了复杂性?
  • @Gusman 您确实需要循环 - 在(罕见的)碰撞情况下您需要从头开始重做
  • @MarcGravell 用他的代码,是的,但问题的关键是避免锁,最后的循环与自旋锁相同,所以使用提供的机制并不是更好而不是自己滚动?
  • @Gusman 我能看到的代码中唯一的循环是“重复直到成功”循环,它是必需的,与旋转无关;我错过了什么吗?

标签: c# arrays multithreading concurrency interlocked


【解决方案1】:

是的,它仍然是原子的和线程安全的。对同一单元格的任何调用都会将相同的地址传递给双精度。像它是否在一个对象的一个​​字段的数组中这样的细节是无关紧要的。

但是,这行:

double newCurrentValue = items[i][j];

不是原子的 - 理论上可能会给出一个撕裂的值(尤其是在 x86 上)。在这种情况下这实际上没问题,因为在撕裂值场景中,它只会命中循环,算作碰撞,然后重做 - 这次使用来自 CompareExchange 的已知原子值。

【讨论】:

    【解决方案2】:

    似乎您最终希望为数组项添加一些值。

    我想只有值的更新(数组本身保持同一块内存)并且所有更新都是通过这个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.ReadInterlocked.CompareExchange

    【讨论】:

    • 而且这里的 SpinLock 没有多大意义。在大多数情况下,循环本身就足够了。
    • 非常感谢您的建议!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-14
    • 2019-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-27
    相关资源
    最近更新 更多