reference assignment is atomic so why is Interlocked.Exchange(ref Object, Object) needed?
Asked Answered
C

4

131

In my multithreaded asmx web service I had a class field _allData of my own type SystemData which consists of few List<T> and Dictionary<T> marked as volatile. The system data (_allData) is refreshed once in a while and I do it by creating another object called newData and fill it's data structures with new data. When it's done I just assign

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

This should work since the assignment is atomic and the threads that have the reference to old data keep using it and the rest have the new system data just after assignment. However my collegue said that instead of using volatile keyword and simple assigment I should use InterLocked.Exchange because he said that on some platforms it's not guaranteed that reference assignment is atomic. Moreover: when I declare the _allData field as volatile the

Interlocked.Exchange<SystemData>(ref _allData, newData); 

produces warning "a reference to a volatile field will not be treated as volatile" What should I think about this?

Cruelty answered 3/2, 2010 at 13:23 Comment(0)
S
214

There are numerous questions here. Considering them one at a time:

reference assignment is atomic so why is Interlocked.Exchange(ref Object, Object) needed?

Reference assignment is atomic. Interlocked.Exchange does not do only reference assignment. It does a read of the current value of a variable, stashes away the old value, and assigns the new value to the variable, all as an atomic operation.

my colleague said that on some platforms it's not guaranteed that reference assignment is atomic. Was my colleague correct?

No. Reference assignment is guaranteed to be atomic on all .NET platforms.

My colleague is reasoning from false premises. Does that mean that their conclusions are incorrect?

Not necessarily. Your colleague could be giving you good advice for bad reasons. Perhaps there is some other reason why you ought to be using Interlocked.Exchange. Lock-free programming is insanely difficult and the moment you depart from well-established practices espoused by experts in the field, you are off in the weeds and risking the worst kind of race conditions. I am neither an expert in this field nor an expert on your code, so I cannot make a judgement one way or the other.

produces warning "a reference to a volatile field will not be treated as volatile" What should I think about this?

You should understand why this is a problem in general. That will lead to an understanding of why the warning is unimportant in this particular case.

The reason that the compiler gives this warning is because marking a field as volatile means "this field is going to be updated on multiple threads -- do not generate any code that caches values of this field, and make sure that any reads or writes of this field are not "moved forwards and backwards in time" via processor cache inconsistencies."

(I assume that you understand all that already. If you do not have a detailed understanding of the meaning of volatile and how it impacts processor cache semantics then you don't understand how it works and should not be using volatile. Lock-free programs are very difficult to get right; make sure that your program is right because you understand how it works, not right by accident.)

Now suppose you make a variable which is an alias of a volatile field by passing a ref to that field. Inside the called method, the compiler has no reason whatsoever to know that the reference needs to have volatile semantics! The compiler will cheerfully generate code for the method that fails to implement the rules for volatile fields, but the variable is a volatile field. That can completely wreck your lock-free logic; the assumption is always that a volatile field is always accessed with volatile semantics. It makes no sense to treat it as volatile sometimes and not other times; you have to always be consistent otherwise you cannot guarantee consistency on other accesses.

Therefore, the compiler warns when you do this, because it is probably going to completely mess up your carefully developed lock-free logic.

Of course, Interlocked.Exchange is written to expect a volatile field and do the right thing. The warning is therefore misleading. I regret this very much; what we should have done is implement some mechanism whereby an author of a method like Interlocked.Exchange could put an attribute on the method saying "this method which takes a ref enforces volatile semantics on the variable, so suppress the warning". Perhaps in a future version of the compiler we shall do so.

Selfdiscipline answered 3/2, 2010 at 16:20 Comment(5)
From what I've heard Interlocked.Exchange also guaranties that a memory barrier is created. So if you for example create a new object, then assign a couple of properties and then store the object in another reference without using Interlocked.Exchange then the compiler might mess up the order of those operations, thus making accessing the second reference not thread-safe. Is that really so? Does it make sense to use Interlocked.Exchange is that kind of scenarios?Treblinka
@Mike: When it comes to what is possibly observed in low-lock multithreaded situations I am as ignorant as the next guy. The answer will probably vary from processor to processor. You should address your question to an expert, or read up on the subject if it interests you. Joe Duffy's book and his blog are good places to start. My rule: don't use multithreading. If you must, use immutable data structures. If you can't, use locks. Only when you must have mutable data without locks should you consider low-lock techniques.Selfdiscipline
Thanks for your answer Eric. It does indeed interest me, that's why I've been reading up books and blogs about multithreading and locking strategies and also trying to implement those in my code. But there's still a lot to learn...Treblinka
@EricLippert Between "don't use multithreading" and "if you must, use immutable data structures", I would insert the intermediate and very common level of "have a child thread use only exclusively owned input objects and the parent thread consume the results only when the child is finished". As in var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.Charmaincharmaine
@John: That's a good idea. I try to treat threads like cheap processes: they are there to do a job and produce a result, not to run around being a second thread of control inside the main program's data structures. But if the amount of work the thread is doing is so large that it's reasonable to treat it like a process, then I say just make it a process!Selfdiscipline
S
11

Either your collegue is mistaken, or he knows something that the C# language specification doesn't.

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."

So, you can write to the volatile reference without risk of getting a corrupted value.

You should of course be careful with how you decide which thread should fetch the new data, to minimise the risk that more than one thread at a time does that.

Stormproof answered 3/2, 2010 at 13:32 Comment(5)
@guffa: yes I've read that also. this leaves the original question "reference assignment is atomic so why is Interlocked.Exchange(ref Object, Object) needed?" unasweredCruelty
@zebrabox: what do you mean? when they are not? what would you do?Cruelty
@matti: It's needed when you have to read and write a value as an atomic operation.Stormproof
How often do you have to worry about the memory not being aligned correctly in .NET really? Interop-heavy stuff?Hellenize
@zebrabox: The specification doesn't list that caveat, it gives a very clear statement. Do you have a reference for a non-memory-aligned situation where a reference read or write fails to be atomic? Seems like that would violate the very clear language in the specification.Dorrie
L
7

Interlocked.Exchange< T >

Sets a variable of the specified type T to a specified value and returns the original value, as an atomic operation.

It changes and returns the original value, it's useless because you only want to change it and, as Guffa said, it's already atomic.

Unless a profiler as proven it to be a bottleneck in your application, you should consider using locks, as it's easier to understand and to prove that your code is right.

Linseed answered 3/2, 2010 at 13:40 Comment(0)
L
7

Interlocked.Exchange() is not just atomic, it also takes care of memory visibility:

The following synchronization functions use the appropriate barriers to ensure memory ordering:

Functions that enter or leave critical sections

Functions that signal synchronization objects

Wait functions

Interlocked functions

Synchronization and Multiprocessor Issues

This means that in addition to atomicity it ensures that:

  • For the thread calling it:
    • No reordering of the instructions is done (by the compiler, the run-time or the hardware).
  • For all threads:
    • No reads from memory before this instruction will see changes to memory (by the thread that called this instruction) that happened after this instruction. This may sound obvious but cache lines may be flushed to main memory not in the order they were written to.
    • All reads after this instruction will see the change made by this instruction and all changes made (by the thread that called this instruction) before this instruction.
    • All writes to memory after this instruction will happen after this instruction change has reached the main memory (by flushing this instruction change to main memory when its done and not let the hardware flush it own its on timing).
Literator answered 29/12, 2016 at 15:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.