【问题标题】:Using Interlocked.CompareExchange with a class将 Interlocked.CompareExchange 与类一起使用
【发布时间】:2011-10-05 03:29:36
【问题描述】:

System.Threading.Interlocked.CompareExchange 运算符提供了比较与交换操作的原子(因此是线程安全的)C# 实现。

例如int i = 5; Interlocked.CompareExchange(ref i, 10, 5); 在这个命令之后,int i 的值 = 10。而且比较和交换也是原子发生的(单个操作)。

当我尝试将它与类实例一起使用时,比较失败并且没有交换值。

   public class X
   {
       public int y;
       public X(int val) { y = val; }
   }

当我这样做的时候

    X a = new X(1);
    X b = new X(1);
    X c = new X(2);
    Interlocked.CompareExchange<X>(ref a, c, b);

比较和交换操作失败。因此,我将 X 类的 Equals 和 == 运算符重写为

    public override bool Equals(object obj) { return y == ((X) obj).y; }

所以,现在我得到 Interlocked.Equals(a,b)true,但 CompareExchange 操作仍然失败。

有什么方法可以做到这一点吗?我想比较两个类实例并根据比较为其中一个分配一个值。

【问题讨论】:

  • 我猜 CompareExchange 使用 ReferenceEquals 来比较 a 和 b,这是有道理的。您的 Equals 覆盖仅推荐用于不可变类型,您是否仔细考虑过您想要/需要什么 Equals 行为?
  • 不幸的是,没有记录,在这种情况下如何确定相等性。
  • @Henk 假设我想要上述行为,其中两个类实例在其成员 y 具有相同值时相等。
  • 正如@jaff 下面解释的那样,这不能成为原子的。
  • 你的例子坏了。 “在此命令之后,int i 的值 = 10” - 仅当 CompareExchange 返回的值是 5(或 10 :-))。这可能是简化的示例代码,但我看到了 很多 损坏的代码,人们忘记了在继续之前需要检查 CompareExchange 是否成功。

标签: c# .net multithreading interlocked


【解决方案1】:

没有。做不到。

Interlocked.CompareExchange 基本上直接映射到能够自动比较和交换内存地址内容的汇编指令。我相信在 32 位模式下,可以使用 64 位版本的指令(以及 32 位和 16 位版本),在 64 位模式下,我认为可以使用 128 位版本。但仅此而已。 CPU 没有“根据其特定的Equals 函数交换 .NET 类”指令。

如果你想交换任意对象,使用任意相等函数,你必须自己做,使用锁或其他同步机制。

Interlocked.CompareExchange 函数的 an overload 可用于对象引用,但出于上述原因,它使用 引用相等。它只是比较引用,然后交换它们。

针对您的评论,使用结构不能解决问题。同样,CPU 只能原子地比较和交换某些固定大小的值,并且它没有抽象数据类型的概念。可以使用引用类型,因为引用本身具有有效的大小,并且 CPU 可以将其与另一个引用进行比较。但是 CPU 对引用指向的对象一无所知

【讨论】:

  • 谢谢。由于各种原因,使用锁破坏了使用原子操作的目的,但似乎我没有太多选择。另外,我想知道操作是否支持交换struct,因为它们不是引用类型。
  • @Bhargav:不,它没有,如documentation of that method 中所述:“此方法仅支持引用类型。”
  • @Bhargav:结构也好不到哪里去。我在答案中添加了更多细节,希望能对其进行解释。 CPU 只是盲目地比较两个引用(基本上是指针或内存地址),然后交换它们。它不知道参考点 to 是什么,也不知道您已经定义了比较这些对象的特殊方法。它将两个引用视为不透明的数据块,并检查它们是否存储相同的位模式(它们指向同一个对象)
【解决方案2】:

Interlocked.CompareExchange的正常使用是在模式中:

SomeType oldValue;
do
{
  oldValue = someField;
  someType newValue = [Computation based on oldValue]
} while (CompareExchange(ref someField, newValue, oldValue) != oldValue);

基本思想是,如果字段在读入oldValue 和处理CompareExchange 之间没有发生变化,那么newValue 将保存应该存储到字段中的值.如果在计算过程中有其他东西改变了它,计算的结果将被放弃,并使用新的值重复计算。假设计算速度很快,最终的效果是允许任意计算表现得好像它是原子的。

如果您想使用 Equals() 相等性进行比较交换式操作,您可能应该执行以下操作:

SomeType newValue = desired new value;
SomeType compareValue = desired comparand;
SomeType oldValue;
do
{
  oldValue = someField;
  if (!oldValue.Equals(compareValue) return oldValue;
} while (CompareExchange(ref someField, newValue, oldValue) != oldValue);
return oldValue;

请注意,如果someField 持有对与compareValue 相等的对象的引用,并且在比较期间将其更改为持有对不同对象的引用,则将根据compareValue 检查该新值.将重复该过程,直到比较报告从字段字段读取的值不等于比较对象,或者直到字段中的值保持不变的时间足以让Equals()CompareExchange 方法完成。

【讨论】:

    【解决方案3】:

    我觉得整个页面有些混乱。首先,评论员是正确的,该问题包含一个危险的假设:

    int i = 5; 
    Interlocked.CompareExchange(ref i, 10, 5);
    

    在此命令之后,int i 的值 = 10

    不,仅当i 的值在此期间没有更改为5 以外的值时。尽管这在此处显示的代码中似乎不太可能,但使用 CompareExchange 的全部意义在于它应该是可能的,因此这里是一个关键的技术性。我担心 OP 可能不理解 Interlocked.CompareExchange 的目的,特别是因为他没有检查返回值(见下文)。

    现在原问题的文字是:

    “有什么方法可以做到这一点吗?我想比较两个类实例并根据比较为其中一个分配一个值。”

    由于“this”这个词没有可行的先行词,我们或许应该将后面的句子视为问题,给出意译:

    “有没有办法比较两个类实例并根据比较为其中一个分配一个值?”

    不幸的是,这个问题仍然不清楚,或者可能与原子操作关系不大。首先,你不能“给 [一个类实例] 赋值”。这没有任何意义。对类实例的引用一个值,但是没有办法将任何东西“分配”给类实例本身。这是与值类型的主要区别,可以相互分配。您可以使用new 运算符创建 一个实例,但您仍然只是获得对它的引用。同样,这些看起来像是技术性问题,但如果该问题真正涉及无锁并发,则它们是关键点。

    接下来,Interlocked.CompareExchange 函数不会根据值来设置存储位置,而是有条件地将值存储到(给定)位置,这意味着它要么存储值(成功),要么保持存储位置不变(失败),同时可靠地指示发生了哪些情况。

    这意味着“基于比较”这一短语对于备选操作的确切含义是不完整的。查看 OP 问题的前面部分,一个最佳猜测可能是该问题正在寻求有条件地操纵实例引用,而原子性是一个红鲱鱼。很难知道,因为如上所述,CompareExchange(用于说明问题)不会“交换”内存中的两个值,它只可能“存储”一个值。

    X a = new X(1);
    X b = new X(1);
    X c = new X(2);
    
    if (a.y == b.y)
        a = c;
    else
        // ???
    

    使用Equals 重载,这可以简化:

    if (a == b)
        a = c;
    else
        // ???
    

    OP 对内部字段平等y 的关注似乎增加了对问题的这种解释走上正轨的可能性。但显然,这些问题的答案与Interlocked.CompareExchange 无关。我们需要更多信息来了解为什么 OP 认为分配必须是原子的。

    另外,我们应该注意,也可以原子交换现有实例中的y 值:

    var Hmmmm = Interlocked.CompareExchange(ref a.y, c.y, b.y);
    

    或交换实例references,现在应该很明显,等同引用仅根据“引用相等”来定义:

    var Hmmmm = Interlocked.CompareExchange(ref a, c, b);
    

    要从这里开始,这个问题需要更清楚。例如,要重述本页其他地方的评论,但更强烈的是,不检查 Interlocked.CompareExchange 的返回值是错误的

    这就是我在上面的示例中存储返回值的原因,以及我认为它的名称是否合适。不根据返回值进行分支就是不了解无锁(“乐观”)并发的基本原理,对此的讨论超出了本问题的范围。有关出色的介绍,请参阅 Joe Duffy 的 Concurrent Programming on Windows

    最后,我认为 OP 不太可能真的需要基于任意考虑以原子方式存储类引用,因为这是一项极其专业的操作,通常仅在全面的无锁系统设计的关键时刻才需要。但是(与另一个答案相反)按照@supercat 描述的方式肯定是可能的。

    所以请不要以为您无法在 .NET 中编写无锁代码,或者认为类引用对于 Interlocked 操作来说是个问题;实际上恰恰相反:如果您确实需要在两个不同的存储位置之间进行选择或以其他方式影响多个内存位置的原子操作,那么使用 纠缠的位置被包装在一个琐碎的包含中的设计很简单class 然后为您提供一个可以以无锁方式原子交换的单个引用。无锁编码在 .NET 中轻而易举,因为在乐观路径失败的极少数情况下,它可以减少内存管理重试对象的麻烦。

    可以这么说,根据我的经验,无锁并发没有我无法在 C#/.NET/CLR 中实现的基本方面,即使有时是边缘有点粗糙,您可以从https://stackoverflow.com/a/5589515/147511 中确定。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-08-23
      • 2013-01-07
      • 1970-01-01
      • 2010-11-01
      • 2010-10-15
      • 2010-12-19
      • 2023-03-16
      • 1970-01-01
      相关资源
      最近更新 更多