Is C# 6 ?. (Elvis op) thread safe? If so, how?
Asked Answered
M

2

34

Apologies in advance: this question comes from a hard-core, unreformed C++ developer trying to learn advanced C#. Consider the following:

if (myUserDefinedObject != null)
{
    myUserDefinedObject.ToString();
}

This is obviously not thread safe. On the other hand, I've seen two tutorials that say ?. (the Null Conditional Operator or 'Elvis Operator') for example,

myUserDefinedObject?.ToString();

IS thread safe. Unless the compiler wraps a [mutex?] lock around it under the covers (shiver), I don't understand how that can be true. If this idiom is thread safe, can someone point me to a technical description of how that is accomplished? If it's not thread safe, does anyone have a reference that actually says it is not?

Maraschino answered 3/3, 2016 at 23:7 Comment(9)
Sorry, but your first block of code can be perfectly thread safe, depending on the context in which it is used and the scope of the variables involved.Stacked
@KenWhite - I think the idea with the first block is that another thread could set the variable to null after the check but before the .ToString() causing the code to fail. I would say that it is not thread-safe.Eyrie
@Enigmativity: The poster specifically said that the block of code was not thread safe, which is not an accurate statement without knowing the context or scope. I pointed out that the statement was inaccurate - it's untrue that the code in that block is definitively not thread safe.Stacked
@KenWhite - I'm not sure that I understand what you mean. I read it as there is a circumstance whereby another thread could cause this code to fail and that's what makes it unsafe.Eyrie
@Enigmativity: I'm saying that the statement This is obviously not thread safe. is not accurate without knowing the context of the code in question. While there are circumstances where the code is unsafe, but there are also circumstances where it can be safe. A general statement that This is obviously not thread safe is incorrect and can be misleading. I just wanted that pointed out for future readers, particularly those who are new to C#.Stacked
@KenWhite - Let me see if this way of thinking about it helps. myUserDefinedObject represents a location in memory (large enough to hold a pointer) that, at the time it was checked, contained zeros. To my knowledge, the only way that location could ever be protected from modification before it is accessed again for the call .ToString(), would be some explicit operation such as a mutex.Maraschino
@KenWhite - Let me see if this way of thinking about it helps. myUserDefinedObject represents a location in memory (large enough to hold a pointer) that, at the time it was checked, contained zeros. To my knowledge, the only way that location could ever be protected from modification before it is accessed again for the call .ToString(), would be some explicit operation such as locking after establishing a memory barrier using atomics and lock guards (as in C and C++). The original code posted here was the poster child for the problem with the double check locking protocol (DCLP) until C++11.Maraschino
@all, apologies for the double post. I hit the wrong key before I was finished editing the first one and can't figure out how to delete. Newbie.Maraschino
@Any, For those that are interested, "Double-Checked Locking is Fixed In C++11" (preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11) has a pretty good explanation of the problem and demonstrates how it is solved in C11 and C++11. It was also solved in Java in, if I remember correctly, about the same timeframe.Maraschino
B
41

I want to clarify BJ Myers's (correct) answer.

In C# an event can be thought of as a field of delegate type -- just as a property can be thought of as a field of the property type -- and the value of that "field" can be null. If you are in the unfortunate situation of having an event handler being modified on one thread while another thread is attempting to invoke it, you can get into the situation where:

if (this.SomeEvent != null) 
    this.SomeEvent( ... );

is not threadsafe. The value could be mutated so that it is non-null before the check, null after the check, and the program crashes.

The usual way to make this "threadsafe", and I use the term advisedly, is to copy the value to a local and then test the local for null. This has the benefit of not crashing with a null dereference. However, the clever developer will note that there is still a race! The sequence can be

  • Non-null event handler cached on thread A
  • Event handler set to null on thread B
  • State needed by event handler is destroyed on thread B
  • Event handler runs on thread A and dies horribly

So in that sense this pattern is not "thread safe". If you are in this unfortunate position you are responsible for ensuring that the appropriate threading logic is implemented so that this cannot happen. You can do that however you want. If you want the (questionable) benefits of being able to call an event handler on one thread while mutating the event on another thread, then you've got to pay either to make it safe, or deal with race condition bugs.

I personally would avoid this situation like the plague, but I'm not smart enough to write correct multithreaded code.

Now, as for the actual question:

some_expression ?. ToString();

is the same as

temp = some_expression
temp == null ? null : temp.ToString()

Is the latter code "threadsafe" in your opinion?

Bung answered 4/3, 2016 at 1:37 Comment(6)
Great comments here - thanks for your excellent explanation. "Thread safety" depends entirely on the way that threads are using the code.Erudition
So... is NOT tread safe? or is it? Do we need to use a lock to avoid crashes if we have multiple threads that are messing around?Adapter
@Narvalex: How should I know? Explain where you are putting the lock, and what race the lock protects against, and what your strategy is for avoiding deadlocks, and then we can have some shot at determining whether your solution is correct. Remember, thread safety is a property of entire programs, so if you want to know whether a program is threadsafe, you must provide a precis of the entire program. That's why threading is hard. You're holding a brick and asking "is the house this brick is made out of structurally sound?" That's a property of the house, not the bricks.Bung
@Eric Lippert: Thanks for your reasoning. I was getting this wrong. According to this conversation and the answers of this issue I understand that if we create a temporal variable with the handler and check if that is null, is does not matter if the original reference is nullified, because the temporary variable holds the reference and for that, it will not be null after reading it. I did not know that.Adapter
@Narvalex: That's correct; a second read should not be introduced after the initial read determines that the local is not null. But that is only the beginning of your problems, not the end of them. Suppose it is not null and you invoke it, but the original variable has been nulled. Now you are invoking a handler which has been removed after it was removed, but the code that removed it might well be surprised by that fact! If you are in this situation then everyone needs to cooperate to ensure that either this doesn't happen, or if it does, that the stale handler doesn't crash.Bung
Exactly, even though you are confident that this won't result in a NullReferenceException for the handler, it can still result in a faulty status if the handler relies on some resources that has already been removed and in a broken status.Reconstructive
E
32

From MSDN (emphasis mine):

Another use for the null-condition member access is invoking delegates in a thread-safe way with much less code. The old way requires code like the following:

var handler = this.PropertyChanged;
if (handler != null)
    handler(…)

The new way is much simpler:

PropertyChanged?.Invoke(e)

The new way is thread-safe because the compiler generates code to evaluate PropertyChanged one time only, keeping the result in temporary variable.

So there is no locking involved here - thread-safety is enforced by creating a local temporary variable, which prevents a different thread from modifying that variable in between the null check and some other operation.

Erudition answered 3/3, 2016 at 23:50 Comment(3)
Therefore, how thread-safe it is depends on what you want to do. You might get an ObjectDisposedException if another thread calls Dispose(), but you won't get a null pointer exception at the call site.Intricacy
I'd be a lot more inclined to believe the MSDN documentation if someone could explain how, in C#, between the time a memory address is check for non-zero, and some unknowable future time when an attempt is made to access the contents of that address, that content had not been modified. From the article I cited above, we know it can be done using the hardware to 'mark' that location (using atomics, memory_modes, etc.). Is that what "the compiler" is doing? @Hank, you also might get an answer - just not from the object you were expecting to still be at that location. ;-)Maraschino
This isn't really thread safe as explained here refer "Read Introduction" section. JIT can sometimes completely eliminate the local variable and introduce another read to the actual field.Missy

© 2022 - 2024 — McMap. All rights reserved.