2

I have read the threading manual and relevant MSDN pages and SO questions several times. Still, I do not completely understand if Volatile.Read/Write and interlocked operations apply only to the relevant variables, or all read/writes before/after that operations.

E.g., imagine I have an array and a counter.

long counter = 0;
var values = new double[1000000];

values[42] = 3.1415;
// Is this line needed instead of simple assignment above,
// or the implicit full-fence of Interlocked will guarantee that 
// all threads will see the values[42] after interlocked increment?
//Volatile.Write(ref values[42], 3.1415);
Interlocked.Increment(ref counter);

Does interlocked increment guarantees the same result as if I used Volatile.Write(ref values[42], 3.1415); instead of values[42] = 3.1415;.

What if I have an array of reference types, e.g. some POCO, and set an instance fields before interlocked increment. Does the implicit full fence apply to all read/writes from that thread before it, or only to the counter?

I am implementing a scalable reader/writer scheme and I found the following statement in the Joe Duffy post:

If the variables protected are references to heap objects, you need to worry about using the read protection each time you touch a field. Just like locks, this technique doesn’t compose. As with anything other than simple locking, use this technique with great care and caution; although the built-in acquire and release fences shield you from memory model reordering issues, there are some easy traps you can fall into.

Is this just a general statement to discourage using low-lock constructs, or somehow applies to the example above?

Community
  • 1
  • 1
V.B.
  • 6,236
  • 1
  • 33
  • 56
  • You only require acquire/release semantics here to get correct behaviour which interlocked increment provides. the guarantee is that any thread that sees the updated counter will also see the proceeding writes. – Voo Apr 08 '16 at 14:27
  • 1
    If you don't understand it then you should never, *never* use it. It will not work, guaranteed. With the bear trap that you won't find out that it does not work, only your user will. Use the ReaderWriterLockSlim class instead, it was written by somebody that understood it and knew how to make it as optimal as it could be. – Hans Passant Apr 08 '16 at 14:28
  • 1
    @HansPassant there is such thing in the world called `learning`... And SO helps with that :) If you read the entire Joe Duffy post series on reader-writer lock implementations, you will notice order-of-magnitude performance difference. – V.B. Apr 08 '16 at 14:30
  • So you want to be better than Joe Duffy... Well, you are guaranteed to learn something you don't know yet. – Hans Passant Apr 08 '16 at 14:34
  • 1
    @HansPassant I do not want to be better, I just [prefer 200 Mops over 15 Mops](https://github.com/Spreads/Spreads/blob/master/tests/Spreads.Core.Tests/NanoOptimizationTests.cs) when there is no contention... Such answers as "You are too stupid for that" are just [not constructive](http://joeduffyblog.com/2010/09/06/the-premature-optimization-is-evil-myth/). – V.B. Apr 08 '16 at 14:38

1 Answers1

4

What you are probably missing is an understanding of fences. This is the best resource to read up on them: http://www.albahari.com/threading/part4.aspx

The short answer is Interlocked.Increment issues a full fence which is independent of the variable it is updating. I believe Volatile.Write issues a half fence. A half fence can be constructed from Thread.MemoryBarrier. When we say Interlocked.Increment issues a full fence it means that Thread.MemoryBarrier is called before and after the operation. Volatile.Write calls Thread.MemoryBarrier before the write and Volatile.Read after. The fences determine when memory access can be reordered (and it's not variable specific as Thread.MemoryBarrier is parameterless).

keith
  • 5,122
  • 3
  • 21
  • 50
  • So am I safe here? – V.B. Apr 08 '16 at 14:26
  • On top, interlocked is atomic without additional locking. You can not repro it without a lock with any other operation as the value may change beween read and write. Yes, you are safe. – TomTom Apr 08 '16 at 14:30
  • It would be redundant to put `Thread.MemoryBarrier` in between the call to `values[42] = 3.1415;` and `Interlocked.Increment(ref counter);` so adding Volatile.Write would not add anything to your code. But you probably should read the link I provided. If `values[42]` could be read from a different thread you could end up with a torn read unless you use a guaranteed atomic operation to read/write it. I don't think the CLR makes any guarentees about the atomicity of reading/writing a double accross platform architectures (for instance consider a double as two 32 bit values on a 32 bit platform). – keith Apr 08 '16 at 14:31
  • @keith I started my question with that link, btw. I am reading values only after the counter is incremented. So yes, I didn't get the fences thing correctly. – V.B. Apr 08 '16 at 14:34
  • Sorry I duplicated your link. If you are reading values only after the counter is increment you will get the same results and just as long as nothing is accessing values[42] before the counter is incremented you should be good. A full post of your code would probably be a good idea if you have any further worries about the concurrency of your complete solution. Keep in mind that it's virtually impossible to do lock free code correctly with anything other than a trivial problem. – keith Apr 08 '16 at 14:44
  • @keith POC code is [here](https://github.com/Spreads/Spreads/blob/master/src/Spreads.Core/Collections/Vector.cs#L187). I want to make all mutable structures in that library thread-safe and lock-free. For this kind of numeric stuff locks are just too slow. – V.B. Apr 08 '16 at 14:47
  • Thanks, just had a quick skim of that code. If the setter of Capacity can be accessed by different threads, you've definitely got a race condition in there and I would post that up as a new question. – keith Apr 08 '16 at 15:00
  • Only item getter/setter is implemented yet - just to test the performance. – V.B. Apr 08 '16 at 15:02
  • @V.B. the only reason to use something like Joe's implementation is if you want arbitrary reader/writers. your implementation is incredibly inefficient for single read/writes. Even if you need larger read/writes later I'd think hard about avoiding the overhead for the simple variant, although combining the two things gets complicated quickly. – Voo Apr 08 '16 at 18:51
  • It is the most efficient for single writer/multiple readers. For uncontended read the overhead is near the cost of a virtual call via interfaces. With some design contract I could avoid syncronization for bulk load, and use of only for real time. There are no such data streams where interlocked would be slow. – V.B. Apr 08 '16 at 19:08
  • @keith, this answer has an incorrect statement. You write that "`Volatile.Write` calls `Thread.MemoryBarrier` after the write...", which is not the case: both `Volatile.Write` and `Thread.VolatileWrite` issue the `MemoryBarrier` ***before*** the memory access, as you can verify with Reflector in .NET 4.6.1 (and earlier versions as I recall). For an excellent explanation of the reason for this, you can refer to Joe Duffy's book, page 514. – Glenn Slayden Aug 12 '16 at 20:42
  • @Glenn Slayden, that's very observant, although you didn't spot that Volatile.Read issues the memory barrier after instead of before i.e. they were both the wrong way round. Have updated. – keith Aug 13 '16 at 13:01