是空合并运算符(?)在C#中的线程安全的吗? [英] Is the null coalescing operator (??) in C# thread-safe?

查看:184
本文介绍了是空合并运算符(?)在C#中的线程安全的吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有以下code的竞争条件,可能导致的NullReferenceException

- 或 -

是否有可能为回调变量被设置为空的空合并运算符检查后,空值,但该函数被调用之前?

  MyClass类{
    公益行动回调{获得;组; }
    公共无效DoCallback(){
        (回拨??新的行动(()=> {}))();
    }
}
 

修改

这是一个问题,是因为出于好奇。我通常不code这个样子。

我不担心回调变量变得陈旧。我担心的异常 DoCallback 被抛出。

编辑#2

下面是我的类:

  MyClass类{
    操作回调{获得;组; }
    公共无效DoCallbackCoalesce(){
        (回拨??新的行动(()=> {}))();
    }
    公共无效DoCallbackIfElse(){
        如果(空=回拨!)回调();
        其他新的动作(()=> {})();
    }
}
 

方法 DoCallbackIfElse 有可能会抛出一个的NullReferenceException 的竞争条件。请问 DoCallbackCoalesce 方法具有相同的条件?

这里是IL输出:

  MyClass.DoCallbackCoalesce:
IL_0000:ldarg.0
IL_0001:拨打UserQuery + MyClass.get_Callback
IL_0006:DUP
IL_0007:brtrue.s IL_0027
IL_0009:流行
IL_000A:ldsfld UserQuery + MyClass.CS $<> 9__CachedAnonymousMethodDelegate1
IL_000F:brtrue.s IL_0022
IL_0011:ldnull
。IL_0012:ldftn UserQuery + MyClass的< D​​oCallbackCoalesce> 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:拨打UserQuery + MyClass.get_Callback
IL_0006:brfalse.s IL_0014
IL_0008:ldarg.0
IL_0009:拨打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的< D​​oCallbackIfElse> 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
 

在我看来像拨打UserQuery + MyClass.get_Callback 使用时只获取调用一次 ?? 运营商,而是两次在使用如果...否则。难道我做错了什么?

解决方案

 公共无效DoCallback(){
    (回拨??新的行动(()=> {}))();
}
 

时,保证是等效于:

 公共无效DoCallback(){
    操作本地=回拨;
    如果(本地== NULL)
       当地=新的行动(()=> {});
    本地();
}
 

无论这可能会导致一个NullReferenceException取决于内存模式。 Microsoft .NET Framework的内存模型文档永远不会引入额外的读取,因此该值测试对是将要调用同一个值,你的code是安全。 然而,ECMA-335 CLI内存模型是那么严格,允许运行,消除局部变量,并访问回调场两次(我假定这是一个场或属性访问一个简单的领域)。

您应该标记回调字段挥发性,以确保正确的内存屏障使用 - 这使得code,即使在弱ECMA-335机型安全。

如果这不是性能的关键code,只用一个锁(读回调到锁内的局部变量就足够了,你不需要保存在调用委托锁) - 什么都需要详细了解关于内存型号知道它是否是安全的,确切的细节可能会在将来的.NET版本的更改(Java不同,微软并没有完全指定.NET存储模式)。

Is there a race condition in the following code that could result in a NullReferenceException?

-- or --

Is it possible for the Callback variable to be set to null after the null coalescing operator checks for a null value but before the function is invoked?

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

EDIT

This is a question that arose out of curiosity. I don't normally code this way.

I'm not worried about the Callback variable becoming stale. I'm worried about an Exception being thrown from DoCallback.

EDIT #2

Here is my class:

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

The method DoCallbackIfElse has a race condition that may throw a NullReferenceException. Does the DoCallbackCoalesce method have the same condition?

And here is the IL output:

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    

It looks to me like call UserQuery+MyClass.get_Callback is only getting called once when using the ?? operator, but twice when using if...else. Am I doing something wrong?

解决方案

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

is guaranteed to be equivalent to:

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

Whether this may cause a NullReferenceException depends on the memory model. The Microsoft .NET framework memory model is documented to never introduce additional reads, so the value tested against null is the same value that will be invoked, and your code is safe. However, the ECMA-335 CLI memory model is less strict and allows the runtime to eliminate the local variable and access the Callback field twice (I'm assuming it's a field or a property that accesses a simple field).

You should mark the Callback field volatile to ensure the proper memory barrier is used - this makes the code safe even in the weak ECMA-335 model.

If it's not performance critical code, just use a lock (reading Callback into a local variable inside the lock is sufficient, you don't need to hold the lock while invoking the delegate) - anything else requires detailed knowledge about memory models to know whether it is safe, and the exact details might change in future .NET versions (unlike Java, Microsoft hasn't fully specified the .NET memory model).

这篇关于是空合并运算符(?)在C#中的线程安全的吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆