10

I have read at different places people saying one should always use lock instead of volatile. I found that there are lots of confusing statements about Multithreading out there and even experts have different opinions on some things here.

After lots of research I found that the lock statement also will insert MemoryBarriers at least.

For example:

    public bool stopFlag;

    void Foo()
    {
        lock (myLock)
        {
          while (!stopFlag)
          {
             // do something
          }
        }
    }

But if I am not totally wrong, the JIT compiler is free to never actually read the variable inside the loop but instead it may only read a cached version of the variable from a register. AFAIK MemoryBarriers won't help if the JIT made a register assignment to a variable, it just ensures that if we read from memory that the value is current.

Unless there is some compiler magic saying something like "if a code block contains a MemoryBarrier, register assignment of all variables after the MemoryBarrier is prevented".

Unless declared volatile or read with Thread.VolatileRead(), if myBool is set from another Thread to false, the loop may still run infinite, is this correct? If yes, wouldn't this apply to ALL variables shared between Threads?

codymanix
  • 28,510
  • 21
  • 92
  • 151
  • 1
    "if myBool is set from another Thread to false" - how that "other" thread will enter to region protected by the same `myLock`? (assuming you *correctly* protect your variables with locks) – Alexei Levenkov Apr 09 '15 at 16:35
  • Even if this would work as written (and currently it wouldn't) it would be a *terrible* idea. It'd be a busy wait. You should be using a synchronization primitive to wait, if you need to wait on another thread. – Servy Apr 09 '15 at 16:35
  • 1
    @AlexeiLevenkov My guess is that assumption of yours is in fact false. – Servy Apr 09 '15 at 16:36
  • @Servy I'm quite sure that my assumption is wrong... :) but I can't come up with expression that says it politely. – Alexei Levenkov Apr 09 '15 at 16:38
  • No, there is no guarantee that the compiler or JITer won't generate code that just reads `myBool` once and then keep it in a register. What are you trying to do? Out of context, your code snippet makes no sense. Are you trying to use `myLock` to synchronize access to `myBool`? If so, then no other thread that follows the rules would be able to modify `myBool` here, because your loop is holding the lock. – Jim Mischel Apr 09 '15 at 16:43
  • As some of you obviously misunderstood my point, I edited my post. – codymanix Apr 10 '15 at 08:31
  • The code inside the loop does not know it is under a lock. It might be part of an unrelated method. Therefore it can't behave differently. – usr Apr 10 '15 at 09:09

3 Answers3

8

Whenever I see a question like this, my knee-jerk reaction is "assume nothing!" The .NET memory model is quite weak and the C# memory model is especially noteworthy for using language that can only apply to a processor with a weak memory model that isn't even supported anymore. Nothing what's there tells you anything what's going to happen in this code, you can reason about locks and memory barriers until you're blue in the face but you don't get anywhere with it.

The x64 jitter is quite clean and rarely throws a surprise. But its days are numbered, it is going to be replaced by Ryujit in VS2015. A rewrite that started with the x86 jitter codebase as a starting point. Which is a concern, the x86 jitter can throw you for a loop. Pun intended.

Best thing to do is to just try it and see what happens. Rewriting your code a little bit and making that loop as tight as possible so the jitter optimizer can do anything it wants:

class Test {
    public bool myBool;
    private static object myLock = new object();
    public int Foo() {
        lock (myLock) {
            int cnt = 0;
            while (!myBool) cnt++;
            return cnt;
        }
    }
}

And testing it like this:

    static void Main(string[] args) {
        var obj = new Test();
        new Thread(() => {
            Thread.Sleep(1000);
            obj.myBool = true;
        }).Start();
        Console.WriteLine(obj.Foo());
    }

Switch to the Release build. Project + Properties, Build tab, tick the "Prefer 32-bit" option. Tools + Options, Debugging, General, untick the "Suppress JIT optimization" option. First run the Debug build. Works fine, program terminates after a second. Now switch to the Release build, run and observe that it deadlocks, the loop never completes. Use Debug + Break All to see that it hangs in the loop.

To see why, look at the generated machine code with Debug + Windows + Disassembly. Focusing on the loop only:

                int cnt = 0;
013E26DD  xor         edx,edx                      ; cnt = 0
                while (myBool) {
013E26DF  movzx       eax,byte ptr [esi+4]         ; load myBool 
013E26E3  test        eax,eax                      ; myBool == true?
013E26E5  jne         013E26EC                     ; yes => bail out
013E26E7  inc         edx                          ; cnt++
013E26E8  test        eax,eax                      ; myBool == true?
013E26EA  jne         013E26E7                     ; yes => loop
                }
                return cnt;

The instruction at address 013E26E8 tells the tale. Note how the myBool variable is stored in the eax register, cnt in the edx register. A standard duty of the jitter optimizer, using the processor registers and avoiding memory loads and stores makes the code much faster. And note that when it tests the value, it still uses the register and does not reload from memory. This loop can therefore never end and it will always hang your program.

Code is pretty fake of course, nobody will ever write this. In practice this tends to work by accident, you'll have more code inside the while() loop. Too much to allow the jitter to optimize the variable way entirely. But there are no hard rules that will tell you when this happens. Sometimes it does pull it off, assume nothing. Proper synchronization should never be skipped. You really are only safe with an extra lock for myBool or an ARE/MRE or Interlocked.CompareExchange(). And if you want to cut such a volatile corner then you must check.

And noted in the comments, try Thread.VolatileRead() instead. You need to use a byte instead of a bool. It still hangs, it is not a synchronization primitive.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Just a guess: MRE=ManualResetEvent. ARE=AutoResetEvent – codymanix Apr 10 '15 at 09:46
  • So it means I have to always use volatile or Thread.VolatileRead() to get the actual value of the variable, even though Iam inside a lock statement, right? – codymanix Apr 10 '15 at 12:58
  • It simply means that you cannot arbitrarily ignore proper synchronization. You got a pretty good demonstration that the lock statement did absolutely squat to make that bool thread-safe. If you use lock, or whatever other synchronization primitive you prefer, then it needs to synchronize access to that bool, not the while() loop. And no, Thread.VolileRead() doesn't cut it either, it is not a synchronization primitive. You can find out for yourself by using the debugging technique I demonstrated. – Hans Passant Apr 10 '15 at 13:17
  • Sure, synchronisation events would be better style but this wasn't my question. Why would Thread.VolatileRead() not work? It should guarantee that I always read the current value. – codymanix Apr 10 '15 at 13:53
  • Because it is not a synchronization primitive. It will very reliably give you the value that's stored in the register, not the value in memory. Try it. – Hans Passant Apr 10 '15 at 13:56
  • @HansPassant VolatileRead does force a memory load (as I just tested to make sure). So does `Volatile.Read`. I'm not sure what kind of barrier it forces but it doesn't matter. x86 implements it as a full barrier. ECMA says that volatile accesses are to be treated like side-effects (that must happen in order). – usr Apr 10 '15 at 17:56
  • 1
    No it doesn't, I checked the machine code *and* I tested it. Hangs like a mother. – Hans Passant Apr 10 '15 at 17:57
  • @HansPassant I tested it as well and inspected the disassembly. .NET 4.5, x64, module optimized. http://pastebin.com/u5DL4nAK. The disassembly shows a call instruction. I assume the volatile read was not inlined. – usr Apr 10 '15 at 18:04
  • Quote: "The x64 jitter is quite clean and rarely throws a surprise". Well, it did throw this surprise :) You must use the x86 jitter to repro this problem. – Hans Passant Apr 10 '15 at 18:05
  • @HansPassant if it really did, which I'm yet to witness, it's a bug. – usr Apr 10 '15 at 18:05
  • It is not a bug, a jitter is allowed to do this. – Hans Passant Apr 10 '15 at 18:06
  • Not in the presence of volatile accesses. Are we talking about the same code here?! Did you inspect the pastebin link? – usr Apr 10 '15 at 18:08
  • If Thread.VolatileRead() would not guarantee that the read value is not the current value from memory it would be in fact useless as the programmer is not able to control whether a register assignment ocurs or not. I agree with usr, if this would happen, this would be a bug. – codymanix Apr 13 '15 at 09:16
  • [MSDN](https://msdn.microsoft.com/en-us/library/bah54t54(v=vs.110).aspx) says it explicitly: **Even on a uniprocessor system, VolatileRead and VolatileWrite ensure that a value is read or written to memory, and not cached (for example, in a processor register)** – codymanix Apr 13 '15 at 09:19
  • No need to shout at me, I didn't do it. I'm just showing what's really happening. – Hans Passant Apr 13 '15 at 09:26
  • Sorry I didn't want to shout, just wanted to highlight. – codymanix Apr 13 '15 at 09:40
  • @HansPassant I just tested your code with a "volatile" keyword in the "bool" variable. It then works as expected. I am running in RELEASE mode with Prefer x86 enabled, and not suppressing JIT optimization on module load. – Giovani Luigi Feb 11 '21 at 21:47
6

the JIT compiler is free to never actually read the variable inside the loop but instead it may only read a cached version of the variable from a register.

Well, it'll read the variable once, in the first iteration of the loop, but other than that, yes, it will continue to read a cached value, unless there is a memory barrier. Any time the code crosses a memory barrier it cannot use the cached value.

Using Thread.VolatileRead() adds the appropriate memory barriers, as does marking the field as volatile. There are plenty of other things that one could do that also implicitly add memory barriers; one of them is entering or leaving a lock statement.`

Since your loop is saying within the body of a single lock and not entering or leaving it, it's free to continue using the cached value.

Of course, the solution here isn't to add in a memory barrier. If you want to wait for another thread to notify you of when you should continue on, use a AutoResetEvent (or another similar synchronization tool specifically designed to allow threads to communicate).

Servy
  • 202,030
  • 26
  • 332
  • 449
  • a MemoryBarrier will not help when the JIT made a register assignment to the variable. A MemoryBarrier only ensures that **if** the variable is read from memory that its value is current. – codymanix Apr 10 '15 at 08:25
0

how about this

public class Singleton<T> where T : class, new()
{
    private static T _instance;
    private static object _syncRoot = new Object();

    public static T Instance
    {
        get
        {
            var instance = _instance;
            if (instance == null)
            {
                lock (_syncRoot)
                {
                    instance = Volatile.Read(ref _instance);
                    if (instance == null)
                    {
                        instance = new T();
                    }
                    Volatile.Write(ref _instance, instance);
                }
            }
            return instance;
        }
    }
}
yuuhhe
  • 1