7

Question:

There are patterns (such as the one here C#/CLR: MemoryBarrier and torn reads ) that can execute torn reads, but never use the resulting value if a torn read may have occurred.

Is this undefined behavior in C#?

Related: How could I determine for myself if this is undefined behavior or not?

Failed attempt to determine the answer:

My understanding (correct my if I'm wrong) is that in C++11 this would be undefined behavior because its not defined by the memory model which was added in C++11 (in older versions all multi-threading was implementation specific behavior). Theoretically I could determine this from the spec.

I tried to do this for C# and failed. I could not find the memory model in the spec. Below are some notes from my unsuccessful journey through the C# language specification (http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf ) attempting to track down the semantics of assigning one struct to another in the case where there might be a data race (one reader copying, one writer writing to the data being copied). I failed to find even the semantics for assigning to or reading from any variable in any scenario. Either I missed something (almost certainly the case) or using variables is undefined behavior.

My notes while reading relevant parts of the C# Specification:

The C# language specification says what types are atomic but never what it meas to do atomic and non-atomic reads and writes:

12.5 Atomicity of variable references

Reads and writes of the following data types shall be atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types. In addition, reads and writes of enum types with an underlying type in the previous list shall also be atomic. Reads and writes of other types, including long, ulong, double, and decimal, as well as user-defined types, need not be atomic. Aside from the library functions designed for that purpose, there is no guarantee of atomic read-modify-write, such as in the case of increment or decrement.

It does not say if "atomic" (which it does not define) here implies any fences (so I assume not), but more relevantly for this question, it does not even define what read and write do (Which is non-trivial in multi-threaded programs).

Looking 14.14.1 Simple assignment, this seems to be the extent of the specification of write (explaining "x = y"):

The value resulting from the evaluation and conversion of y is stored into the location given by the evaluation of x.

8.3 Variables and parameters states:

A variable shall be assigned before its value can be obtained.

but does not define obtaining a value. No where do I see a spec for what reading produces (One would assume that last thing you wrote in the single threaded case, so I can't find that in the spec).

10.10 Execution order seems to unnecessarily constrain (compared to normal acquire and release semantics used in the MSDN article linked below) volatile with respect to writes (no writes can move in either direction across a reference to a volatile) while under constraining reads (they can move in both direction across volatile operations. It also makes no mention of Thread.MemoryBarrier (Who's documentation seems prevent processor but not compiler reordering so its too weak). It also makes no reference to what loading from a variable/field means, so lots of nasty offtopic issues there, but no answers.

I've read all the parts of the spec that I could find that are relevant. Nowhere is reading from a field/memory/variable (looks like "variable" is the proper term here) defined.

Maybe somewhere in the language spec there is a spec for the behavior of read/load and write/store of variables (aka: the memory model), but if there is (I couldn't find it), it does not reference "atomic" (I searched it for atomic: section 12.5 is the only use). I don't see how any C# code can possibly be defined, so clearly I'm missing something: I don't think a valid C# implementation could just exit (due to it being undefined behavior!) on any read or write of a value.

If multi-threaded (and maybe even single threaded) C# is actually horribly under-specified with no memory-model, is there a go-to place to see specifications for particular implementations? Ex: If C# does not define the semantics of read and write, perhaps Microsoft's various C# compilers (There are at least 3 now) provide specifications, and Mono as well? Is this safe in any of the implementations and whats a good way to tell?

Maybe there are some informal (not in the spec) rules that are considered safe to go by (all major implementations comply)? That would be scary, but if that's all we get, I'll take it.

Some relevant but insufficient sources:

I found this article claiming to be about the C# memory model, but it is implementation specific (Refers to the CLR) and does not cover the case in question. It also provides a nice clean explanation of what I'd like the volatile semantics to be (C++11 style acquire an release), but is stronger in some ways and weaker in other than 10.10 Execution order from the language spec, so I think its wrong: https://msdn.microsoft.com/magazine/jj863136

This article seems to have a lot of good information comparing C# memory model to C++11's, but also does not cover this specific issue as far as I can tell: http://blog.alexrp.com/2014/03/30/dot-net-atomics-and-memory-model-semantics/

A nice article about reordering issues. The update at the end mentions that "A volatile read can be moved backwards in time with respect to a volatile write" which disagrees with 10.10 Execution order from the spec, but agrees with what most people see to claim the semantics should be (meaning it is in agreement with the MSDN article and the Volatile class, but not the keyword as far as I can tell): http://blog.coverity.com/2014/03/12/can-skip-lock-reading-integer/#.VoSyfhXhDGg

From a quick look this article mostly covers volatile and how it prevents redundant load elimination and why that's needed: https://blogs.msdn.microsoft.com/ericlippert/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three/

CraigM
  • 389
  • 2
  • 5
  • Eric Lippert touches this subject I think [here](https://blogs.msdn.microsoft.com/ericlippert/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three/) and [here](http://blog.coverity.com/2014/03/12/can-skip-lock-reading-integer/#.VoSyfhXhDGg) – rene Dec 31 '15 at 04:47
  • Torn reads generally imply that reading a variable requires more than one machine instruction (i.e. accessing a `long` field on 32-bit machines). Isn't what you're calling a torn read actually a race? – Kirill Shlenskiy Dec 31 '15 at 05:55
  • @rene: thanks. Ive added those to the question. – CraigM Dec 31 '15 at 05:58
  • @Kirill Shlenskiy: In this case the read is assumed to be some potentially non atomic read (ex: arbitrary struct as in the linked example https://stackoverflow.com/questions/18935939/c-clr-memorybarrier-and-torn-reads ) In this case you have a data race between a reader and a writer that can produce a torn read (and or undefined behavior?). The special thing here though is that after doing the read (and before using the read value) it determines if there may have been a data-race and throws it away if there was. – CraigM Dec 31 '15 at 06:02
  • @CraigM, sorry for a bit of an off-topic, but I find that entire linked example quite baffling - even more so given that it appears to be written by Sandro Magi (Sasa lib author), whom I respect greatly. Different copies of the struct will only share the same `write` object, whereas `value` and `version` will be copied, with different copies eventually going out of sync, making it unsuitable for synchronisation. As far a I can see the only way to use `Sync` concurrently is to change it to a class (as passing the struct between different threads will inevitably involve a copy and break it). – Kirill Shlenskiy Dec 31 '15 at 11:13
  • @Kirill Shlenskiy, The Sync must never be copied. What does get copied is the T value inside of it (which is some custom struct, presumably too large to read atomically). When you do a read, that T value is copied during in a loop (which may get a torn read) until no writers touched it while copying (that last read must have been not torn), then returned (which of course copies it again, but that's not important). You could (with horrible perf) do the same thing while locking around each access of version and remove all other synchronization: that would be equivalent but much slower. – CraigM Dec 31 '15 at 17:15

0 Answers0