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

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

– 要么 –

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

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

编辑

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

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

编辑#2

这是我的class级:

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

方法DoCallbackIfElse具有可能抛出NullReferenceException的竞争条件。 DoCallbackCoalesce方法是否具有相同的条件?

这是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.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.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时两次。 难道我做错了什么?

 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字段两次(我假设它是一个字段或访问一个简单字段的属性)。

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

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

更新

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

null-coalescing运算符导致Callback仅被评估一次。 代表是不可变的 :

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

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

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

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

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

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

原始答案

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

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

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

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

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

编写DoCallback更清晰的方法是:

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

如果Callbacknull ,它也比原始代码快一点,因为它不会创建和调用no-op委托。


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

  public event Action Callback; 

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

在类似事件的字段上调用+=时,会发生对事件的Subscribe方法的调用。 对于类似事件的事件,事件订阅保证是primefaces的。 在实践中,它使用一些Interlocked技术来做到这一点。


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

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