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(); }
如果Callback
为null
,它也比原始代码快一点,因为它不会创建和调用no-op委托。
并且您可能希望通过事件替换属性,以获得primefaces+=
和-=
:
public event Action Callback;
在属性上调用+=
时,会发生什么是Callback = Callback + someMethod
。 这不是primefaces的,因为可以在读取和写入之间更改Callback
。
在类似事件的字段上调用+=
时,会发生对事件的Subscribe
方法的调用。 对于类似事件的事件,事件订阅保证是primefaces的。 在实践中,它使用一些Interlocked
技术来做到这一点。
使用null合并运算符??
在这里并不重要,它本身也不是线程安全的。 重要的是你只读过一次Callback
。 还有其他类似的模式涉及??
这在任何方面都不是线程安全的。
我们假设它是安全的,因为它是一条线? 通常情况并非如此。 在访问任何共享内存之前,您确实应该使用lock语句。