Is the 'volatile' keyword still broken in C#?
Asked Answered
D

4

31

Joe Albahari has a great series on multithreading that's a must read and should be known by heart for anyone doing C# multithreading.

In part 4 however he mentions the problems with volatile:

Notice that applying volatile doesn’t prevent a write followed by a read from being swapped, and this can create brainteasers. Joe Duffy illustrates the problem well with the following example: if Test1 and Test2 run simultaneously on different threads, it’s possible for a and b to both end up with a value of 0 (despite the use of volatile on both x and y)

Followed by a note that the MSDN documentation is incorrect:

The MSDN documentation states that use of the volatile keyword ensures that the most up-to-date value is present in the field at all times. This is incorrect, since as we’ve seen, a write followed by a read can be reordered.

I've checked the MSDN documentation, which was last changed in 2015 but still lists:

The volatile keyword indicates that a field might be modified by multiple threads that are executing at the same time. Fields that are declared volatile are not subject to compiler optimizations that assume access by a single thread. This ensures that the most up-to-date value is present in the field at all times.

Right now I still avoid volatile in favor of the more verbose to prevent threads using stale data:

private int foo;
private object fooLock = new object();
public int Foo {
    get { lock(fooLock) return foo; }
    set { lock(fooLock) foo = value; }
}

As the parts about multithreading were written in 2011, is the argument still valid today? Should volatile still be avoided at all costs in favor of locks or full memory fences to prevent introducing very hard to produce bugs that as mentioned are even dependent on the CPU vendor it's running on?

Dah answered 2/12, 2016 at 18:27 Comment(7)
What's the point of surrounding return and assignment statement by lock in your example?Detonator
This is still misleading. Volatile provides acquire/release memory semantics which are enough to implement many algorithms efficiently. Yes it's difficult to use, but that doesn't make it broken (c++'s solution is clearly superior but they had the advantage of seeing the problems with volatile in java) (And anyone who thinks that memory barriers are easier than volatile just have not enough experience with architectures other than x86. Try to use memory barriers on an architecture without multi-copy atomicity and see how far you get)Ilanailangilang
(The msdn description is admittedly noticeably worse. Clearly whoever wrote that snippet does not understand volatile at all and one can only hope was never allowed to write a single line of concurrent code)Ilanailangilang
@Detonator lock provides full before and after memory fencing. A memory fence ensures the CPU cache is refreshed from memory. For example I use this in a while(!Stopped) for background service work on a separate thread. If the windows service stops, I put Stopped to true so the separate thread jumps out of the loop. Without locking (and thus fencing) the backing field of the property might remain cached in the CPU, will never be refreshed and the thread will never stop.Dah
@Detonator I could have written a Thread.MemoryBarrier() before and after the statement, but with the return it becomes fiddly (then it's even larger with try ... finally)Dah
Currently your locks don't really do anything more than a volatile does, and in any case can't prevent many classes of synchronization problems. Most of these relate to the interleaving of two threads - for instance, attempting to do something like foo++ from two threads simultaneously can still have undefined results, and why should be more obvious given your locking example: there's a separate lock for each the load and store, meaning two threads have to fight over who goes first for each operation (whether this is relevant to your current code using this is unknown at this time).Paraphrastic
@Paraphrastic I am aware that race conditions can occur on the value. I use that snippet mostly to ensure a while loop terminates. If Thread A sets it to true and Thread B didn't realize it the first time it will just continue the loop until the next check. The problem I'm trying to solve here is preventing stale values that are never updated on Thread B and never stops even though Thread A set it to true a long time ago. But you're right, I could have used volatile for that purpose.Dah
I
32

Volatile in its current implementation is not broken despite popular blog posts claiming such a thing. It is however badly specified and the idea of using a modifier on a field to specify memory ordering is not that great (compare volatile in Java/C# to C++'s atomic specification that had enough time to learn from the earlier mistakes). The MSDN article on the other hand was clearly written by someone who has no business talking about concurrency and is completely bogus.. the only sane option is to completely ignore it.

Volatile guarantees acquire/release semantics when accessing the field and can only be applied to types that allow atomic reads and writes. Not more, not less. This is enough to be useful to implement many lock-free algorithms efficiently such as non-blocking hashmaps.

One very simple sample is using a volatile variable to publish data. Thanks to the volatile on x, the assertion in the following snippet cannot fire:

private int a;
private volatile bool x;

public void Publish()
{
    a = 1;
    x = true;
}

public void Read()
{
    if (x)
    {
        // if we observe x == true, we will always see the preceding write to a
        Debug.Assert(a == 1); 
    }
}

Volatile is not easy to use and in most situations you are much better off to go with some higher level concept, but when performance is important or you're implementing some low level data structures, volatile can be exceedingly useful.

Ilanailangilang answered 2/12, 2016 at 19:23 Comment(9)
Did you intend "a" to be volatile, or does volatile on "x" ensure the write to "a" happened?Mitrailleuse
@Patrick The code is correct as-is. Memory ordering guarantees are much stricter than "writes cannot be cached" and this plays in here too. To vastly simplify: If thread B sees the update to the volatile variable X, it is guaranteed to see all writes that happened before on thread A when it wrote the value to x. This allows us to use a single volatile bool to publish other data.Ilanailangilang
Does volatile really only have acquire/release semantics? Or sequential consistency? I would've thought the latter, and the two aren't the same.Anjelicaanjou
@Mehrdad The C# specification does indeed guarantee acquire/release and does not guarantee sequential consistency. (Section 10.5.3 of the spec). Sequential consistency is an expensive guarantee to offer, so they don't offer it.Tarryn
Wow, I didn't know. Thanks!Anjelicaanjou
"Volatile guarantees atomicity" - this is wrong. Volatile doesn't do anything with atomicity (see ECMA-335 specification)Energumen
@Valery Right, confused it with the JMM. It does guarantee though that you can only apply volatile to a type that is guaranteed to be atomic, but that should be phrased better.Ilanailangilang
"Volatile guarantees acquire/release semantics [...]. Not more, not less." -- It's probably more. Quoting a recent GitHub issue: "A fence has optimization-suppressing effects." For example a compiler optimization suppressed by the volatile is to read the field just once, and then use the cached value for the remainder of the program's execution.Declamation
@Theodor That's the difference between what a specific implementation currently does and what the spec guarantees.Ilanailangilang
G
13

As I read the MSDN documentation, I believe it is saying that if you see volatile on a variable, you do not have to worry about compiler optimizations screwing up the value because they reorder the operations. It doesn't say that you are protected from errors caused by your own code executing operations on separate threads in the wrong order. (although admittedly, the comment is not clear as to this.)

Goodwin answered 2/12, 2016 at 18:40 Comment(4)
I agree. I think the emphasis should be "Fields that are declared volatile are not subject to compiler optimizations that assume access by a single thread. This ensures that the most up-to-date value is present in the field at all times."Tartrate
@MikeSherrill'CatRecall': "Compiler" is a red herring. Thread safety is an issue that reaches far beyond the compiler. Re-ordering in the CPU is just as bad, for example.Anjelicaanjou
Every compiler must understand CPU reordering rules. If instructions A and B can be reordered by the CPU, but language semantics dictate otherwise, then the compiler must introduce some type of fence which prevents the reordering, or choose alternative instructions to achieve the same goal. Hence, where volatile semantics prohinit reordering, it's up to the compiler to not just avoid reordering itself, but also prevent the CPU from doing so.Epicalyx
@MSalters: Sure, but I don't think most people would count { reorderings by the CPU that the compiler could have prevented, but didn't } as a kind of "compiler optimization". So Mehrdad's point is that it's not just a matter of certain compiler optimizations not applying to certain fields, but also a matter of compiler having to take steps to disable hardware optimizations.Commonalty
F
2

volatile is a very limited guarantee. It means that the variable isn't subject to compiler optimizations that assume access from a single thread. This means that if you write into a variable from one thread, then read it from another thread, the other thread will definitely have the latest value. Without volatile, one a multiprocessor machine without volatile, the compiler may make assumptions about single-threaded access, for example by keeping the value in a register, which prevents other processors from having access to the latest value.

As the code example you've mentioned shows, it doesn't protect you from having methods in different blocks reordered. In effect volatile makes each individual access to a volatile variable atomic. It doesn't make any guarantees as to the atomicity of groups of such accesses.

If you just want to ensure that your property has an up-to-date single value, you should be able to just use volatile.

The problem comes in if you try to perform multiple parallel operations as if they were atomic. If you have to force several operations to be atomic together, you need to lock the whole operation. Consider the example again, but using locks:

class DoLocksReallySaveYouHere
{
  int x, y;
  object xlock = new object(), ylock = new object();

  void Test1()        // Executed on one thread
  {
    lock(xlock) {x = 1;}
    lock(ylock) {int a = y;}
    ...
  }

  void Test2()        // Executed on another thread
  {
    lock(ylock) {y = 1;}
    lock(xlock) {int b = x;}
    ...
  }
}

The locks cause may cause some synchronization, which may prevent both a and b from having value 0 (I have not tested this). However, since both x and y are locked independently, either a or b can still non-deterministically end up with a value of 0.

So in the case of wrapping the modification of a single variable, you should be safe using volatile, and would not really be any safer using lock. If you need to atomically perform multiple operations, you need to use a lock around the entire atomic block, otherwise scheduling will still cause non-deterministic behavior.

Faden answered 2/12, 2016 at 19:24 Comment(3)
"It basically means that the variable isn't cached"... sigh and another person perpetuating this myth :( No that's absolutely not what volatile guarantees and as a matter of fact volatile fields are cached perfectly fine on x86 and other architectures. Also Volatile absolutely does limit how memory accesses can be reordered which is essential to making it useful. Even if there was a sane definition of "not cached" (there isn't) this would be completely useless for absolutely every algorithm.Ilanailangilang
@Ilanailangilang Just saying what I was taught, sorry. Is this more correct?: "It means that the variable isn't subject to compiler optimizations that assume access from a single thread. [...] Without volatile, one a multiprocessor machine without volatile, the compiler may make assumptions about single-threaded access, for example by keeping the value in a register, which prevents other processors from having access to the latest value."Faden
That is correct, but misses the essential parts of what volatile does. Volatile guarantees acquire/release semantics on reads and writes. For example you cannot reorder a write after a volatile write (you can reorder it before a volatile write just fine though). If you want to start understanding the whole thing you could for example begin with this. It's about the JMM, but the CLR MM is pretty similar in practice.Ilanailangilang
N
0

Here are some useful disassemblies for volatile in C#: https://sharplab.io/#gist:625b1181356b543157780baf860c9173

On x86 it is just about:

  • using memory instead of registers
  • preventing compiler optimizations like in the case with the endless loop

I use volatile when I just want to tell compiler that a field might be updated from many different threads and I do not need additional features provided by interlocked operations.

Nonnah answered 18/4, 2021 at 9:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.