C#中的空合并运算符(??)是否是线程安全的?

时间:2012-05-12 17:14:24

标签: c# .net multithreading thread-safety null-coalescing-operator

以下代码中是否存在可能导致NullReferenceException的竞争条件?

- 或 -

在空合并运算符检查空值之后但在调用函数之前,是否可以将Callback变量设置为null?

class MyClass {
    public Action Callback { get; set; }
    public void DoCallback() {
        (Callback ?? new Action(() => { }))();
    }
}

修改

这是一个出于好奇而产生的问题。我通常不会这样编码。

我并不担心Callback变量会变得陈旧。我担心从Exception抛出DoCallback

编辑#2

这是我的班级:

class MyClass {
    Action Callback { get; set; }
    public void DoCallbackCoalesce() {
        (Callback ?? new Action(() => { }))();
    }
    public void DoCallbackIfElse() {
        if (null != Callback) Callback();
        else new Action(() => { })();
    }
}

方法DoCallbackIfElse的竞争条件可能会引发NullReferenceExceptionDoCallbackCoalesce方法是否具有相同的条件?

这是IL输出:

MyClass.DoCallbackCoalesce:
IL_0000:  ldarg.0     
IL_0001:  call        UserQuery+MyClass.get_Callback
IL_0006:  dup         
IL_0007:  brtrue.s    IL_0027
IL_0009:  pop         
IL_000A:  ldsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate1
IL_000F:  brtrue.s    IL_0022
IL_0011:  ldnull      
IL_0012:  ldftn       UserQuery+MyClass.<DoCallbackCoalesce>b__0
IL_0018:  newobj      System.Action..ctor
IL_001D:  stsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate1
IL_0022:  ldsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate1
IL_0027:  callvirt    System.Action.Invoke
IL_002C:  ret         

MyClass.DoCallbackIfElse:
IL_0000:  ldarg.0     
IL_0001:  call        UserQuery+MyClass.get_Callback
IL_0006:  brfalse.s   IL_0014
IL_0008:  ldarg.0     
IL_0009:  call        UserQuery+MyClass.get_Callback
IL_000E:  callvirt    System.Action.Invoke
IL_0013:  ret         
IL_0014:  ldsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate3
IL_0019:  brtrue.s    IL_002C
IL_001B:  ldnull      
IL_001C:  ldftn       UserQuery+MyClass.<DoCallbackIfElse>b__2
IL_0022:  newobj      System.Action..ctor
IL_0027:  stsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate3
IL_002C:  ldsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate3
IL_0031:  callvirt    System.Action.Invoke
IL_0036:  ret    

我认为call UserQuery+MyClass.get_Callback仅在使用??运算符时调用一次,而在使用if...else时调用两次。我做错了吗?

4 个答案:

答案 0 :(得分:11)

更新

如果我们在编辑澄清时排除了获取陈旧值的问题,那么null-coalescing选项将始终可靠地(即使无法确定确切的行为)。替代版本(如果不是null然后调用它)却不会,并冒着NullReferenceException的风险。

null-coalescing运算符导致Callback仅被评估一次。 Delegates are immutable

  

组合操作(例如“组合”和“删除”)不会更改   现有代表。相反,这样的操作返回一个新的委托   包含操作的结果,未更改的委托,或   空值。组合操作在结果时返回null   operation是一个不引用至少一个方法的委托。一个   组合操作在请求时返回一个未更改的委托   操作没有效果。

此外,委托是引用类型,所以简单的读或写保证是原子的(C#语言规范,第5.5段):

  

以下数据类型的读写是原子的:bool,char,   byte,sbyte,short,ushort,uint,int,float和reference types。

这确认了null-coalescing运算符无法读取无效值,并且因为只有在不存在错误的情况下才会读取该值。

另一方面,条件版本一次读取委托,然后调用第二次独立读取的结果。如果第一次读取返回非空值,但在第二次读取发生之前,委托是(原子地,但没有帮助)用null覆盖,编译器最终调用null Invoke引用,因此会抛出异常。

所有这些都反映在IL的两种方法中。

原始答案

如果没有相反的明确文件,那么是的,这里存在竞争条件,因为在更简单的情况下也会存在

public int x = 1;

int y = x == 1 ? 1 : 0;

原理是相同的:首先评估条件,然后生成表达式的结果(以后使用)。如果发生了导致情况发生变化的事情,那就太晚了。

答案 1 :(得分:10)

public void DoCallback() {
    (Callback ?? new Action(() => { }))();
}

保证等同于:

public void DoCallback() {
    Action local = Callback;
    if (local == null)
       local = new Action(() => { });
    local();
}

这是否会导致NullReferenceException取决于内存模型。记录Microsoft .NET框架内存模型永远不会引入额外的读取,因此针对null测试的值与将要调用的值相同,并且您的代码是安全的。 但是,ECMA-335 CLI内存模型不太严格,允许运行时消除局部变量并访问Callback字段两次(我假设它是一个字段或访问简单字段的属性)。 / p>

您应该标记Callback字段volatile以确保使用正确的内存屏障 - 这使得即使在弱ECMA-335模型中代码也是安全的。

如果它不是性能关键代码,只需使用一个锁(读取锁内部的局部变量回调就足够了,调用委托时不需要保持锁) - 其他任何需要有关内存模型的详细知识要知道它是否安全,并且确切的细节可能会在未来的.NET版本中发生变化(与Java不同,Microsoft尚未完全指定.NET内存模型)。

答案 2 :(得分:3)

我在此代码中看不到竞争条件。有一些潜在的问题:

  • Callback += someMethod;不是原子的。简单的任务是。
  • DoCallback可以调用陈旧的值,但它会保持一致。
  • 只能通过在整个回调期间保持锁定来避免陈旧值问题。但这是一种非常危险的模式,会引发死锁。

更清晰的写作方式DoCallback将是:

public void DoCallback()
{
   var callback = Callback;//Copying to local variable is necessary
   if(callback != null)
     callback();
}

如果Callbacknull,原始代码的速度也会快一些,因为它不会创建和调用无操作代理。


您可能希望通过事件替换属性,以获得原子+=-=

 public event Action Callback;

在属性上调用+=时,会发生什么Callback = Callback + someMethod。这不是原子的,因为在读取和写入之间可能会更改Callback

在类似字段的事件上调用+=时,会发生对事件的Subscribe方法的调用。对于类似事件的事件,事件订阅保证是原子的。在实践中,它使用一些Interlocked技术来执行此操作。


空合并运算符??的使用在这里并不重要,它本身也不是线程安全的。重要的是你只阅读Callback一次。还有其他类似的模式涉及??,它们在任何方面都不是线程安全的。

答案 3 :(得分:0)

我们假设它是安全的,因为它是一条线?通常情况并非如此。你真的应该在访问任何共享内存之前使用lock语句。