null coalesce操作符线程安全吗?

时间:2022-02-26 01:29:32

So this is the meat of the question: Can Foo.Bar ever return null? To clarify, can '_bar' be set to null after it's evaluated as non-null and before it's value is returned?

这就是问题的实质:能喷吗?酒吧会返回null吗?要澄清的是,在'_bar'被计算为非空值并返回值之前,它是否可以设置为null ?

    public class Foo
    {
        Object _bar;
        public Object Bar
        {
            get { return _bar ?? new Object(); }
            set { _bar = value; }
        }
    }

I know using the following get method is not safe, and can return a null value:

我知道使用下面的get方法是不安全的,可以返回一个空值:

            get { return _bar != null ? _bar : new Object(); }

UPDATE:

更新:

Another way to look at the same problem, this example might be more clear:

另一种看待同一问题的方式,这个例子可能更清楚:

        public static T GetValue<T>(ref T value) where T : class, new()
        {
            return value ?? new T();
        }

And again asking can GetValue(...) ever return null? Depending on your definition this may or may not be thread-safe... I guess the right problem statement is asking if it is an atomic operation on value... David Yaw has defined the question best by saying is the above function the equivalent to the following:

再问一次GetValue(…)是否会返回null?根据您的定义,这可能是线程安全的,也可能不是线程安全的。我猜正确的问题陈述是问它是否是关于价值的原子操作…David Yaw已经明确地定义了这个问题,他说,上面的函数等价于下面的函数:

        public static T GetValue<T>(ref T value) where T : class, new()
        {
            T result = value;
            if (result != null)
                return result;
            else
                return new T();
        }

4 个解决方案

#1


22  

No, this is not thread safe.

不,这不是线程安全的。

The IL for the above compiles to:

上述资料摘要汇编为:

.method public hidebysig specialname instance object get_Bar() cil managed
{
    .maxstack 2
    .locals init (
        [0] object CS$1$0000)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldfld object ConsoleApplication1.Program/MainClass::_bar
    L_0007: dup 
    L_0008: brtrue.s L_0010
    L_000a: pop 
    L_000b: newobj instance void [mscorlib]System.Object::.ctor()
    L_0010: stloc.0 
    L_0011: br.s L_0013
    L_0013: ldloc.0 
    L_0014: ret 
}

This effectively does a load of the _bar field, then checks its existence, and jumps ot the end. There is no synchronization in place, and since this is multiple IL instructions, it's possible for a secondary thread to cause a race condition - causing the returned object to differ from the one set.

这将有效地加载_bar字段,然后检查其存在性,并跳转到末尾。这里没有同步,而且由于这是多个IL指令,所以辅助线程有可能导致一个竞争条件——导致返回的对象与一个集合不同。

It's much better to handle lazy instantiation via Lazy<T>. That provides a thread-safe, lazy instantiation pattern. Granted, the above code is not doing lazy instantiation (rather returning a new object every time until some time when _bar is set), but I suspect that's a bug, and not the intended behavior.

最好通过lazy 处理惰性实例化。它提供了线程安全的、延迟的实例化模式。当然,上面的代码并没有执行惰性实例化(而是每次都返回一个新对象,直到设置了_bar),但是我怀疑这是一个bug,而不是预期的行为。

In addition, Lazy<T> makes setting difficult.

此外,懒惰的 使得设置变得困难。

To duplicate the above behavior in a thread-safe manner would require explicit synchronization.

以线程安全的方式复制上述行为需要显式同步。


As to your update:

对于你的更新:

The getter for the Bar property could never return null.

Bar属性的getter永远不会返回null。

Looking at the IL above, it _bar (via ldfld), then does a check to see if that object is not null using brtrue.s. If the object is not null, it jumps, copies the value of _bar from the execution stack to a local via stloc.0, and returns - returning _bar with a real value.

查看上面的IL,它的_bar(通过ldfld),然后使用brtrue.s检查该对象是否为null。如果对象不是null,它会跳转,通过stloc将_bar的值从执行堆栈复制到本地。返回-返回_bar,并具有一个真正的值。

If _bar was unset, then it will pop it off the execution stack, and create a new object, which then gets stored and returned.

如果_bar是未设置的,那么它将从执行堆栈中弹出,并创建一个新对象,然后将其存储并返回。

Either case prevents a null value from being returned. However, again, I wouldn't consider this thread-safe in general, since it's possible that a call to set happening at the same time as a call to get can cause different objects to be returned, and it's a race condition as which object instance gets returned (the set value, or a new object).

这两种情况都可以防止返回空值。然而,再一次,我不会考虑这个线程安全的,因为它可能调用发生在同一时间设置为调用返回可以导致不同的对象,这是一个竞争条件作为哪一个对象实例被返回(设置值,或一个新对象)。

#2


4  

I wouldn't use the word 'thread safe' to refer to this. Instead, I would ask the question, which of these is the same as the null coalesce operator?

我不会用“线程安全”这个词来指代它。相反,我想问的是,这两个中的哪个与null coalesce操作符相同?

get { return _bar != null ? _bar : new Object(); }

or

get
{
    Object result = _bar;
    if(result == null)
    {
        result = new Object();
    }
    return result;
}

From reading the other responses, it looks like it compiles to the equivalent to the second, not the first. As you noted, the first could return null, but the second one never will.

从读取其他响应来看,它似乎编译成第二个响应,而不是第一个。正如您所提到的,第一个可能返回null,但是第二个永远不会返回。

Is this thread safe? Technically, no. After reading _bar, a different thread could modify _bar, and the getter would return a value that's out of date. But from how you asked the question, I think this is what you're looking for.

这是线程安全吗?从技术上讲,没有。读取_bar之后,另一个线程可以修改_bar, getter将返回一个过期的值。但是从你问的问题来看,我认为这就是你要找的。

Edit: Here's a way to do this that avoids the whole problem. Since value is a local variable, it can't be changed behind the scenes.

编辑:这里有一种方法可以避免整个问题。因为值是一个局部变量,所以不能在幕后更改它。

public class Foo
{
    Object _bar = new Object();
    public Object Bar
    {
        get { return _bar; }
        set { _bar = value ?? new Object(); }
    }
}

Edit 2:

编辑2:

Here's the IL I see from a Release compile, with my interpretation of the IL.

这是我从发布编译中看到的IL,我对IL的解释。

.method public hidebysig specialname instance object get_Bar_NullCoalesce() cil managed
{
    .maxstack 8
    L_0000: ldarg.0                         // Load argument 0 onto the stack (I don't know what argument 0 is, I don't understand this statement.)
    L_0001: ldfld object CoalesceTest::_bar // Loads the reference to _bar onto the stack.
    L_0006: dup                             // duplicate the value on the stack.
    L_0007: brtrue.s L_000f                 // Jump to L_000f if the value on the stack is non-zero. 
                                            // I believe this consumes the value on the top of the stack, leaving the original result of ldfld as the only thing on the stack.
    L_0009: pop                             // remove the result of ldfld from the stack.
    L_000a: newobj instance void [mscorlib]System.Object::.ctor()
                                            // create a new object, put a reference to it on the stack.
    L_000f: ret                             // return whatever's on the top of the stack.
}

Here's what I see from the other ways of doing it:

以下是我从其他方法中看到的:

.method public hidebysig specialname instance object get_Bar_IntermediateResultVar() cil managed
{
    .maxstack 1
    .locals init (
        [0] object result)
    L_0000: ldarg.0 
    L_0001: ldfld object CoalesceTest::_bar
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: brtrue.s L_0010
    L_000a: newobj instance void [mscorlib]System.Object::.ctor()
    L_000f: stloc.0 
    L_0010: ldloc.0 
    L_0011: ret 
}

.method public hidebysig specialname instance object get_Bar_TrinaryOperator() cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: ldfld object CoalesceTest::_bar
    L_0006: brtrue.s L_000e
    L_0008: newobj instance void [mscorlib]System.Object::.ctor()
    L_000d: ret 
    L_000e: ldarg.0 
    L_000f: ldfld object CoalesceTest::_bar
    L_0014: ret 
}

In the IL, it's obvious that it's reading the _bar field twice with the trinary operator, but only once with the null coalesce and the intermediate result var. In addition, the IL of the null coalesce method is very close to the intermediate result var method.

在IL中,很明显,它使用三元算符读取了_bar字段两次,但使用null coalesce和中间结果var只读取了一次。此外,null coalesce方法的IL与中间结果var方法非常接近。

And here's the source I used to generate these:

这是我用来产生这些的来源:

public object Bar_NullCoalesce
{
    get { return this._bar ?? new Object(); }
}

public object Bar_IntermediateResultVar
{
    get
    {
        object result = this._bar;
        if (result == null) { result = new Object(); }
        return result;
    }
}

public object Bar_TrinaryOperator
{
    get { return this._bar != null ? this._bar : new Object(); }
}

#3


2  

The getter will never return null.

getter永远不会返回null。

This is because when the read is performed on the variable (_bar) the expression is evaluated and the resulting object (or null) is then "free" of the variable (_bar). It is the result of this first evaluation which is then "passed" to the coalesce operator. (See Reed's good answer for the IL.)

这是因为当对变量(_bar)执行读操作时,表达式将被求值,结果对象(或null)将被“释放”。它是第一个评估的结果,然后将其“传递”给coalesce操作符。(参见里德对《IL》的回答。)

However, this is not thread-safe and an assignment may easily be lost for the same reason as above.

但是,这不是线程安全的,而且由于上述原因,很容易丢失赋值。

#4


0  

Reflector says no:

反射器没有说:

List<int> l = null;
var x = l ?? new List<int>();

Compiles to:

编译:

[STAThread]
public static void Main(string[] args)
{
    List<int> list = null;
    if (list == null)
    {
        new List<int>();
    }
}

Which does not appear to be thread safe in the respect you've mentioned.

在您提到的方面,它似乎并不安全。

#1


22  

No, this is not thread safe.

不,这不是线程安全的。

The IL for the above compiles to:

上述资料摘要汇编为:

.method public hidebysig specialname instance object get_Bar() cil managed
{
    .maxstack 2
    .locals init (
        [0] object CS$1$0000)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldfld object ConsoleApplication1.Program/MainClass::_bar
    L_0007: dup 
    L_0008: brtrue.s L_0010
    L_000a: pop 
    L_000b: newobj instance void [mscorlib]System.Object::.ctor()
    L_0010: stloc.0 
    L_0011: br.s L_0013
    L_0013: ldloc.0 
    L_0014: ret 
}

This effectively does a load of the _bar field, then checks its existence, and jumps ot the end. There is no synchronization in place, and since this is multiple IL instructions, it's possible for a secondary thread to cause a race condition - causing the returned object to differ from the one set.

这将有效地加载_bar字段,然后检查其存在性,并跳转到末尾。这里没有同步,而且由于这是多个IL指令,所以辅助线程有可能导致一个竞争条件——导致返回的对象与一个集合不同。

It's much better to handle lazy instantiation via Lazy<T>. That provides a thread-safe, lazy instantiation pattern. Granted, the above code is not doing lazy instantiation (rather returning a new object every time until some time when _bar is set), but I suspect that's a bug, and not the intended behavior.

最好通过lazy 处理惰性实例化。它提供了线程安全的、延迟的实例化模式。当然,上面的代码并没有执行惰性实例化(而是每次都返回一个新对象,直到设置了_bar),但是我怀疑这是一个bug,而不是预期的行为。

In addition, Lazy<T> makes setting difficult.

此外,懒惰的 使得设置变得困难。

To duplicate the above behavior in a thread-safe manner would require explicit synchronization.

以线程安全的方式复制上述行为需要显式同步。


As to your update:

对于你的更新:

The getter for the Bar property could never return null.

Bar属性的getter永远不会返回null。

Looking at the IL above, it _bar (via ldfld), then does a check to see if that object is not null using brtrue.s. If the object is not null, it jumps, copies the value of _bar from the execution stack to a local via stloc.0, and returns - returning _bar with a real value.

查看上面的IL,它的_bar(通过ldfld),然后使用brtrue.s检查该对象是否为null。如果对象不是null,它会跳转,通过stloc将_bar的值从执行堆栈复制到本地。返回-返回_bar,并具有一个真正的值。

If _bar was unset, then it will pop it off the execution stack, and create a new object, which then gets stored and returned.

如果_bar是未设置的,那么它将从执行堆栈中弹出,并创建一个新对象,然后将其存储并返回。

Either case prevents a null value from being returned. However, again, I wouldn't consider this thread-safe in general, since it's possible that a call to set happening at the same time as a call to get can cause different objects to be returned, and it's a race condition as which object instance gets returned (the set value, or a new object).

这两种情况都可以防止返回空值。然而,再一次,我不会考虑这个线程安全的,因为它可能调用发生在同一时间设置为调用返回可以导致不同的对象,这是一个竞争条件作为哪一个对象实例被返回(设置值,或一个新对象)。

#2


4  

I wouldn't use the word 'thread safe' to refer to this. Instead, I would ask the question, which of these is the same as the null coalesce operator?

我不会用“线程安全”这个词来指代它。相反,我想问的是,这两个中的哪个与null coalesce操作符相同?

get { return _bar != null ? _bar : new Object(); }

or

get
{
    Object result = _bar;
    if(result == null)
    {
        result = new Object();
    }
    return result;
}

From reading the other responses, it looks like it compiles to the equivalent to the second, not the first. As you noted, the first could return null, but the second one never will.

从读取其他响应来看,它似乎编译成第二个响应,而不是第一个。正如您所提到的,第一个可能返回null,但是第二个永远不会返回。

Is this thread safe? Technically, no. After reading _bar, a different thread could modify _bar, and the getter would return a value that's out of date. But from how you asked the question, I think this is what you're looking for.

这是线程安全吗?从技术上讲,没有。读取_bar之后,另一个线程可以修改_bar, getter将返回一个过期的值。但是从你问的问题来看,我认为这就是你要找的。

Edit: Here's a way to do this that avoids the whole problem. Since value is a local variable, it can't be changed behind the scenes.

编辑:这里有一种方法可以避免整个问题。因为值是一个局部变量,所以不能在幕后更改它。

public class Foo
{
    Object _bar = new Object();
    public Object Bar
    {
        get { return _bar; }
        set { _bar = value ?? new Object(); }
    }
}

Edit 2:

编辑2:

Here's the IL I see from a Release compile, with my interpretation of the IL.

这是我从发布编译中看到的IL,我对IL的解释。

.method public hidebysig specialname instance object get_Bar_NullCoalesce() cil managed
{
    .maxstack 8
    L_0000: ldarg.0                         // Load argument 0 onto the stack (I don't know what argument 0 is, I don't understand this statement.)
    L_0001: ldfld object CoalesceTest::_bar // Loads the reference to _bar onto the stack.
    L_0006: dup                             // duplicate the value on the stack.
    L_0007: brtrue.s L_000f                 // Jump to L_000f if the value on the stack is non-zero. 
                                            // I believe this consumes the value on the top of the stack, leaving the original result of ldfld as the only thing on the stack.
    L_0009: pop                             // remove the result of ldfld from the stack.
    L_000a: newobj instance void [mscorlib]System.Object::.ctor()
                                            // create a new object, put a reference to it on the stack.
    L_000f: ret                             // return whatever's on the top of the stack.
}

Here's what I see from the other ways of doing it:

以下是我从其他方法中看到的:

.method public hidebysig specialname instance object get_Bar_IntermediateResultVar() cil managed
{
    .maxstack 1
    .locals init (
        [0] object result)
    L_0000: ldarg.0 
    L_0001: ldfld object CoalesceTest::_bar
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: brtrue.s L_0010
    L_000a: newobj instance void [mscorlib]System.Object::.ctor()
    L_000f: stloc.0 
    L_0010: ldloc.0 
    L_0011: ret 
}

.method public hidebysig specialname instance object get_Bar_TrinaryOperator() cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: ldfld object CoalesceTest::_bar
    L_0006: brtrue.s L_000e
    L_0008: newobj instance void [mscorlib]System.Object::.ctor()
    L_000d: ret 
    L_000e: ldarg.0 
    L_000f: ldfld object CoalesceTest::_bar
    L_0014: ret 
}

In the IL, it's obvious that it's reading the _bar field twice with the trinary operator, but only once with the null coalesce and the intermediate result var. In addition, the IL of the null coalesce method is very close to the intermediate result var method.

在IL中,很明显,它使用三元算符读取了_bar字段两次,但使用null coalesce和中间结果var只读取了一次。此外,null coalesce方法的IL与中间结果var方法非常接近。

And here's the source I used to generate these:

这是我用来产生这些的来源:

public object Bar_NullCoalesce
{
    get { return this._bar ?? new Object(); }
}

public object Bar_IntermediateResultVar
{
    get
    {
        object result = this._bar;
        if (result == null) { result = new Object(); }
        return result;
    }
}

public object Bar_TrinaryOperator
{
    get { return this._bar != null ? this._bar : new Object(); }
}

#3


2  

The getter will never return null.

getter永远不会返回null。

This is because when the read is performed on the variable (_bar) the expression is evaluated and the resulting object (or null) is then "free" of the variable (_bar). It is the result of this first evaluation which is then "passed" to the coalesce operator. (See Reed's good answer for the IL.)

这是因为当对变量(_bar)执行读操作时,表达式将被求值,结果对象(或null)将被“释放”。它是第一个评估的结果,然后将其“传递”给coalesce操作符。(参见里德对《IL》的回答。)

However, this is not thread-safe and an assignment may easily be lost for the same reason as above.

但是,这不是线程安全的,而且由于上述原因,很容易丢失赋值。

#4


0  

Reflector says no:

反射器没有说:

List<int> l = null;
var x = l ?? new List<int>();

Compiles to:

编译:

[STAThread]
public static void Main(string[] args)
{
    List<int> list = null;
    if (list == null)
    {
        new List<int>();
    }
}

Which does not appear to be thread safe in the respect you've mentioned.

在您提到的方面,它似乎并不安全。