13

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?

qxn
  • 17,162
  • 3
  • 49
  • 72
  • 9
    `??` is not "the" ternary operator or "a" ternary operator. It's the null-coalescing operator, which is a binary operator (i.e. it takes 2 operands not 3). The ternary conditional operator is `?:`. – BoltClock May 12 '12 at 17:15
  • 3
    Oh and possible duplicate of [Is the C# '??' operator thread safe?](http://stackoverflow.com/questions/944683/is-the-c-sharp-operator-thread-safe) – BoltClock May 12 '12 at 17:20
  • 3
    Dup of http://stackoverflow.com/questions/4619593/is-the-null-coalesce-operator-thread-safe – Rob P. May 12 '12 at 17:24

4 Answers4

11

Update

If we exclude the problem of getting a stale value as your edit clarifies then the null-coalescing option would always work reliably (even if the exact behavior cannot be determined). The alternate version (if not null then call it) however would not, and is risking a NullReferenceException.

The null-coalescing operator results in Callback being evaluated just once. Delegates are immutable:

Combining operations, such as Combine and Remove, do not alter existing delegates. Instead, such an operation returns a new delegate that contains the results of the operation, an unchanged delegate, or null. A combining operation returns null when the result of the operation is a delegate that does not reference at least one method. A combining operation returns an unchanged delegate when the requested operation has no effect.

Additionally, delegates are reference types so simple reading or writing one is guaranteed to be atomic (C# language specification, para 5.5):

Reads and writes of the following data types are atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types.

This confirms that there is no way that the null-coalescing operator would read an invalid value, and because the value will be read only once there's no possibility of an error.

On the other hand, the conditional version reads the delegate once and then invokes the result of a second, independent read. If the first read returns a non-null value but the delegate is (atomically, but that doesn't help) overwritten with null before the second read happens, the compiler ends up calling Invoke on a null reference, hence an exception will be thrown.

All of this is reflected in the IL for the two methods.

Original Answer

In the absence of explicit documentation to the contrary then yes, there is a race condition here as there would also be in the simpler case

public int x = 1;

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

The principle is the same: first the condition is evaluated, and then the result of the expression is produced (and later used). If something happens that makes the condition change, it's too late.

Jon
  • 428,835
  • 81
  • 738
  • 806
  • +1, Yes, `result = x ?? y` is the same as `if (x == null) result = y; else result = x;` – Olivier Jacot-Descombes May 12 '12 at 17:19
  • 1
    I see no race condition. Perhaps there are some memory model related staleness issues, but no simple race condition. – CodesInChaos May 12 '12 at 17:19
  • I agree with @CodeInChaos: what you are proving is that the value can become stale - or invalid. – IAbstract May 12 '12 at 17:25
  • @CodeInChaos: The way I see it the thread can be preempted just before the result of the expression is invoked and `Callback` changed at this point, making the soon-to-be-called `Action` "wrong". Isn't that a race condition? – Jon May 12 '12 at 17:26
  • The callback will always be invoked on a consistent, but potentially stale value. It's just as thread (un)safe as the standard event subscription pattern. In particular one potential race-condition is that a callback might be called after unsubscribing. – CodesInChaos May 12 '12 at 17:30
  • @Jon: a race condition occurs when a workers are delayed from entering a block of code. – IAbstract May 12 '12 at 17:32
  • Yes, null coalescing is a multi-instruction process and is therefore not thread safe. – Peter Ritchie May 12 '12 at 17:39
  • What CodeInChaos described in his answer (assign to local variable then check the local for null [which could use null coalescing]) is the typical pattern to avoid race conditions – Peter Ritchie May 12 '12 at 17:40
  • @OlivierJacot-Descombes -- See my update. It doesn't look like the IL output of the two operators are the same, unless I did something wrong... – qxn May 12 '12 at 17:49
  • @PeterRitchie The `??` in the original code is just as thread safe as the pattern I describe in my answer. The only difference is performance and readability. – CodesInChaos May 12 '12 at 17:50
  • 1
    The problem is not calling the callback. The problem is that another thread can change the value of `Callback` between the test `Callback != null` or `Callback ??` and actually calling it. The test `Callback != null` can yield `true`, but by the time you call `Callback()` it could have been set to `null`! Note `call UserQuery+MyClass.get_Callback` gets the callback delegate but does not call it. – Olivier Jacot-Descombes May 12 '12 at 17:56
  • @Jon -- Thanks for the update. I'm aware of the NullReferenceException possibility in the method DoCallbackIfElse, which is why I supplied it as an example. – qxn May 13 '12 at 01:25
11
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).

Daniel
  • 15,944
  • 2
  • 54
  • 60
  • Thanks! This is the clearest answer to the question I had in mind. Are you saying that even the equivalent method you gave may throw a NullReferenceException in the ECMA-335 memory model unless the volatile keyword (or a lock) is used? – qxn May 13 '12 at 01:40
  • @ken Yes. The ECMA-335 model makes basically no guarantees at all for unsynchronized non-volatile memory access. (see ECMA-335 §12.6.4) – Daniel May 13 '12 at 10:19
  • I asked another question that pertains to this answer: http://stackoverflow.com/q/10589565/651789 – qxn May 15 '12 at 14:46
3

I see no race condition in this code. There are a few potential issues:

  • Callback += someMethod; is not atomic. Simple assignment is.
  • DoCallback can call a stale value, but it will be consistent.
  • The stale value problem can only be avoided by keeping a lock for the whole duration of the callback. But that is a very dangerous pattern, that invites dead-locks.

A clearer way of writing DoCallback would be:

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

It's also a bit faster that your original code, since it doesn't create and invoke a no-op delegate, if Callback is null.


And you might want to replace the property by an event, to gain atomic += and -=:

 public event Action Callback;

When calling += on a property, what happens is Callback = Callback + someMethod. This is not atomic, since Callback might be changed between the read and the write.

When calling += on a field like event, what happens is a call to the Subscribe method of the event. Event subscription is guaranteed to be atomic for field like events. In practice it uses some Interlocked technique to do this.


The use of the null coalescence operator ?? doesn't really matter here, and it's not inherently thread safe either. What matters is that you read Callback only once. There are other similar patterns involving ?? that are not thread safe in any way.

CodesInChaos
  • 106,488
  • 23
  • 218
  • 262
  • Why would `Callback += someMethod` **not** be atomic while `SomeEvent += someMethod` **is** atomic? – IAbstract May 12 '12 at 17:29
  • 1
    @IAbstract Because for events you don't actually call `Callback = Callback + someMethod`. But rather `Callback.Subscribe(someMethod)`, which for field like events is guaranteed to be atomic. – CodesInChaos May 12 '12 at 17:32
  • Ah ...that's right, thanks for the reminder! ...maybe slide that extra bit of info into your answer so others will know why. :) – IAbstract May 12 '12 at 17:33
  • Thanks. I'm not worried about the staleness of the Callback variable. I'm worried about an exception being thrown in DoCallback due to a null Callback variable (see my update). So, you're saying an exception will never be thrown? – qxn May 12 '12 at 17:57
  • @ken Your `public void DoCallbackIfElse()` is broken, it can throw a `NullReferenceException`. Your original code, and my variant can't. The local variable is essential, so you don't read twice from the `Callback` property. – CodesInChaos May 12 '12 at 19:29
  • @CodeInChaos -- I'm aware of that. That's why I provided it as an example and the resulting IL code. Some of the answers (and comments) seemed to have been implying that the two methods should produce the same IL, which they don't. I now understand that the DoCallbackCoalesce method will never throw a NullReferenceException, which is what I was curious about. Thanks! – qxn May 13 '12 at 01:28
0

We're you assuming it was safe because it is one line? That's usually not the case. You really should use the lock statement before accessing any shared memory.

gcochard
  • 11,408
  • 1
  • 26
  • 41