13

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操作仍然失败。

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

4

3 回答 3

23

不,这是不可能的。

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

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

对对象引用起作用的函数有一个重载,但由于上述原因,它使用引用相等。它只是比较引用,然后交换它们。Interlocked.CompareExchange

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

于 2011-07-14T08:31:49.037 回答
5

的正常使用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方法都完成。

于 2013-02-11T23:54:40.780 回答
5

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

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

在此命令之后,int i 将具有值 = 10

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

现在原始问题的文本是:

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

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

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

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

接下来,该Interlocked.CompareExchange函数不以 value 为条件存储位置,而是有条件地将值存储到(给定的) location,这意味着它要么存储值(成功),要么保持存储位置不变(失败),同时可靠地指出其中哪些发生了。

这意味着“基于比较”这个短语对于备选动作应该是什么是不完整的。查看 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);

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

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

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

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

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

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

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

于 2017-02-01T23:14:46.760 回答