How to correctly read an Interlocked.Increment'ed int field?
Asked Answered
C

5

66

Suppose I have a non-volatile int field, and a thread which Interlocked.Increments it. Can another thread safely read this directly, or does the read also need to be interlocked?

I previously thought that I had to use an interlocked read to guarantee that I'm seeing the current value, since, after all, the field isn't volatile. I've been using Interlocked.CompareExchange(int, 0, 0) to achieve that.

However, I've stumbled across this answer which suggests that actually plain reads will always see the current version of an Interlocked.Incremented value, and since int reading is already atomic, there's no need to do anything special. I've also found a request in which Microsoft rejects a request for Interlocked.Read(ref int), further suggesting that this is completely redundant.

So can I really safely read the most current value of such an int field without Interlocked?

Cretin answered 26/5, 2011 at 13:55 Comment(2)
Care to select an answer? :)B
@B no, I don't feel I can select an answer with confidence as I was never convinced either way. Also there's no point in selecting the top voted answer as it's at the top anyway. At best my tickmark would deceive users into thinking that I know the answer to be correct.Cretin
I
22

If you want to guarantee that the other thread will read the latest value, you must use Thread.VolatileRead(). (*)

The read operation itself is atomic so that will not cause any problems but without volatile read you may get old value from the cache or compiler may optimize your code and eliminate the read operation altogether. From the compiler's point of view it is enough that the code works in single threaded environment. Volatile operations and memory barriers are used to limit the compiler's ability to optimize and reorder the code.

There are several participants that can alter the code: compiler, JIT-compiler and CPU. It does not really matter which one of them shows that your code is broken. The only important thing is the .NET memory model as it specifies the rules that must be obeyed by all participants.

(*) Thread.VolatileRead() does not really get the latest value. It will read the value and add a memory barrier after the read. The first volatile read may get cached value but the second would get an updated value because the memory barrier of the first volatile read has forced a cache update if it was necessary. In practice this detail has little importance when writing the code.

Impound answered 26/5, 2011 at 14:1 Comment(10)
+1, this is useful depending on "why" he cares about an atomic read. (added the MSDN link as well)Hibachi
I must? As in, if I use Interlocked.Read (on a long) or Interlocked.CompareExchange (on an int) then I won't necessarily get the latest value?Cretin
@romkyns: Interlocked methods are safe and they always operate with fresh values. However, if you just read the variable value elsewhere, the value you get is not necessarily the same that you have updated with the Interlocked methods. Even though the Interlocked method has written the value to the main memory, your thread might be executed in another core that has not updated its read cache and you will get the old value. This is really a problem if you use the variable as exit condition for a loop or you expect certain execution order for instructions.Impound
@Impound If that's so, then the answer I linked is wrong. Also, the rejected MS Connect suggestion is not actually redundant. Do you agree?Cretin
I disagree with your assessment of the MS Connect link. Interlocked.Read(Int64) is needed on 32bit cores to ensure you don't get half of the Int64. Only 32bit reads are atomic. It has nothing to do with getting the latest value. You're mixing up synchronization and atomicity.Hibachi
@romkyns: Yes, I think that the answer is wrong if we think about the .NET memory model. Normal read operations can be optimized away if there are no memory barriers so the Interlocked updates are not enough. One must use volatile reads or specify the variable to be volatile.Impound
@romkyns: Like sixlettervariables said, the Interlocked methods exist to provide atomicity so the MS Connect suggestion is rightfully rejected. The synchronization is only a side effect of some Interlocked methods.Impound
@Impound So does Interlocked.Read(Int64) perform a volatile read?Capital
@binki: I.12.6.5 Locks and threads: "5. Explicit atomic operations. The class library provides a variety of atomic operations in the System.Threading.Interlocked class. These operations (e.g. Increment, Decremenet, Exchange, and CompareExchange) perform implicit acquire/release operations." The Ecma specification does not say anything explicit about Interlocked.Read(Int64). I would assume that it does a normal read on 64-bit systems, because the purpose of the Interlocked class is to provide atomicity and the 64-bit reads are already atomic on 64-bit systems. So my answer is no.Impound
According to the documentation, the Thread.VolatileRead is a legacy APIs and has been replaced by the Volatile.Read.Crispen
B
17

A bit of a meta issue, but a good aspect about using Interlocked.CompareExchange(ref value, 0, 0) (ignoring the obvious disadvantage that it's harder to understand when used for reading) is that it works regardless of int or long. It's true that int reads are always atomic, but long reads are not or may be not, depending on the architecture. Unfortunately, Interlocked.Read(ref value) only works if value is of type long.

Consider the case that you're starting with an int field, which makes it impossible to use Interlocked.Read(), so you'll read the value directly instead since that's atomic anyway. However, later in development you or somebody else decides that a long is required - the compiler won't warn you, but now you may have a subtle bug: The read access is not guaranteed to be atomic anymore. I found using Interlocked.CompareExchange() the best alternative here; It may be slower depending on the underlying processor instructions, but it is safer in the long run. I don't know enough about the internals of Thread.VolatileRead() though; It might be "better" regarding this use case since it provides even more signatures.

I would not try to read the value directly (i.e. without any of the above mechanisms) within a loop or any tight method call though, since even if the writes are volatile and/or memory barrier'd, nothing is telling the compiler that the value of the field can actually change between two reads. So, the field should be either volatile or any of the given constructs should be used.

My two cents.

B answered 28/3, 2014 at 13:33 Comment(0)
H
8

You're correct that you do not need a special instruction to atomically read a 32bit integer, however, what that means is you will get the "whole" value (i.e. you won't get part of one write and part of another). You have no guarantees that the value won't have changed once you have read it.

It is at this point where you need to decide if you need to use some other synchronization method to control access, say if you're using this value to read a member from an array, etc.


In a nutshell, atomicity ensures an operation happens completely and indivisibly. Given some operation A that contained N steps, if you made it to the operation right after A you can be assured that all N steps happened in isolation from concurrent operations.

If you had two threads which executed the atomic operation A you are guaranteed you will see only the complete result of one of the two threads. If you want to coordinate the threads, atomic operations could be used to create the required synchronization. But atomic operations in and of themselves do not provide higher level synchronization. The Interlocked family of methods are made available to provide some fundamental atomic operations.

Synchronization is a broader kind of concurrency control, often built around atomic operations. Most processors include memory barriers which allow you to ensure all cache lines are flushed and you have a consistent view of memory. Volatile reads are a way to ensure consistent access to a given memory location.

While not immediately applicable to your problem, reading up on ACID (atomicity, consistency, isolation, and durability) with respect to databases may help you with the terminology.

Hibachi answered 26/5, 2011 at 14:1 Comment(2)
Meta-comment: if your question is what synchronization method should you use @Impound covers it.Hibachi
Should I use any synchronization if I read variable int a right after (in the next line) Interlocked.Increment(a) in the same method?Stichter
S
1

Me, being paranoid, I do Interlocked.Add(ref incrementedField, 0) for int values

Spiritless answered 2/3, 2022 at 14:56 Comment(7)
Most probably being paranoid just costs you a little bit of performance, in exchange for nothing. The Volatile.Read(ref incrementedField) should do the same job more economically. Check out this answer for reference: Difference between Interlocked.Exchange and Volatile.Write?Crispen
In my specific use case, performance loss is unnoticeable (rare calls, couple of times per second), so I can live with that ;)Spiritless
Sure, if performance is not an issue then the Interlocked.Add(ref intVariable, 0) is just as good as the Volatile.Read(ref intVariable), and also easier to reason about its correctness. The problem is that it doesn't communicate clearly its intentions. So you'll have to add comments, in order to help future maintainers understand the intentions of this mysterious line of code. Which will let the maintainers know that you were paranoid, and that you didn't really knew what you were doing. Which is something not very desirable in general. :-)Crispen
@TheodorZoulias, hi! :) Researching the same thing, I've just stumbled upon this and it does make sense to me. So probably not the same as Volatile.Read, in theory. Though I'd probably use Interlocked.CompareExchange(ref val, 0, 0) instead of Interlocked.Add(ref val, 0) for a read operation.Pursy
@Pursy I've posted a relevant answer to this question: Why not volatile on System.Double and System.Long? Judging from the comments in the source code of the Volatile class, the Volatile.Read is atomic too.Crispen
@Pursy I just posted an answer in the question you linked too. I added a cautionary note to be on the safe side. :-)Crispen
@TheodorZoulias tks! I've learnt a lot from both answers 👍Pursy
W
-7

Yes everything you've read is correct. Interlocked.Increment is designed so that normal reads will not be false while making the changes to the field. Reading a field is not dangerous, writing a field is.

Walrath answered 26/5, 2011 at 14:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.