以下代码中是否存在可能导致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
的竞争条件可能会引发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.<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
时调用两次。我做错了吗?
答案 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();
}
如果Callback
为null
,原始代码的速度也会快一些,因为它不会创建和调用无操作代理。
您可能希望通过事件替换属性,以获得原子+=
和-=
:
public event Action Callback;
在属性上调用+=
时,会发生什么Callback = Callback + someMethod
。这不是原子的,因为在读取和写入之间可能会更改Callback
。
在类似字段的事件上调用+=
时,会发生对事件的Subscribe
方法的调用。对于类似事件的事件,事件订阅保证是原子的。在实践中,它使用一些Interlocked
技术来执行此操作。
空合并运算符??
的使用在这里并不重要,它本身也不是线程安全的。重要的是你只阅读Callback
一次。还有其他类似的模式涉及??
,它们在任何方面都不是线程安全的。
答案 3 :(得分:0)
我们假设它是安全的,因为它是一条线?通常情况并非如此。你真的应该在访问任何共享内存之前使用lock语句。