C# variable freshness
Asked Answered
N

2

13

Suppose I have a member variable in a class (with atomic read/write data type):

bool m_Done = false;

And later I create a task to set it to true:

Task.Run(() => m_Done = true);

I don't care when exactly m_Done will be set to true. My question is do I have a guarantee by the C# language specification and the Task parallel library that eventually m_Done will be true if I'm accessing it from a different thread?
Example:

if(m_Done) { // Do something }

I know that using locks will introduce the necessary memory barriers and m_Done will be visible as true later. Also I can use Volatile.Write when setting the variable and Volatile.Read when reading it. I'm seeing a lot of code written this way (without locks or volatile) and I'm not sure if it is correct.

Note that my question is not targeting a specific implementation of C# or .Net, it is targeting the specification. I need to know if the current code will behave similarly if running on x86, x64, Itanium or ARM.

Necessitate answered 6/6, 2015 at 0:30 Comment(3)
I can't help but feel that the point of async/await is to avoid these hard questions...Francophobe
@Francophobe No, it's not. If your code that uses async-await does (or can) run on multiple threads, you still need to synchronize properly.Howlet
@Howlet Depends. You could use await to synchronize and marshal the result to the thread that "owns" the object, and have THAT set the property.Francophobe
L
23

I don't care when exactly m_Done will be set to true. My question is do I have a guarantee by the C# language specification and the Task parallel library that eventually m_Done will be true if I'm accessing it from a different thread?

No.

The read of m_Done is non-volatile and may therefore be moved arbitrarily far backwards in time, and the result may be cached. As a result, it could be observed to be false on every read for all time.

I need to know if the current code will behave similarly if running on x86, x64, Itanium or ARM.

There is no guarantee made by the specification that the code will be observed to do the same thing on strong (x86) and weak (ARM) memory models.

The specification is pretty clear on what guarantees are made about non-volatile reads and writes: that they may be arbitrarily re-ordered on different threads in the absence of certain special events such as locks.

Read the specification for details, particularly the bit about side effects as they relate to volatile access. If you have more questions after that, then post a new question. This is very tricky stuff.

Moreover the question presupposes that you are ignoring the existing mechanisms that determine that a task is completed, and instead rolling your own. The existing mechanisms were designed by experts; use them.

I'm seeing a lot of code written this way (without locks or volatile) and I'm not sure if it is correct.

It almost certainly is not.

A good exercise to pose to the person who wrote that code is this:

static volatile bool q = false;
static volatile bool r = false;
static volatile bool s = false;
static volatile bool t = false;
static object locker = new object();

static bool GetR() { return r; }  // No lock!
static void SetR() { lock(locker) { r = true; } }

static void MethodOne()
{
  q = true;
  if (!GetR())
    s = true;
}

static void MethodTwo()
{
  SetR();
  if (!q)
    t = true;
}

After initialization of the fields, MethodOne is called from one thread, MethodTwo is called from another. Note that everything is volatile and that the write to r is not only volatile, but fully fenced. Both methods complete normally. Is it possible afterwards for s and t to ever both be observed to be true on the first thread? Is it possible on x86? It appears not; if the first thread wins the race then t remains false, and if the second thread wins then s remains false; this analysis is wrong. Why? (Hint: how is x86 permitted to rewrite MethodOne ?)

If the coder is unable to answer this question then they are almost certainly unable to program correctly with volatile, and should not be sharing memory across threads without locks.

Landseer answered 6/6, 2015 at 1:11 Comment(5)
What I have understood is in above program we have four possible results for combination s,t value each being true / false, and it is possible only because of usage of volatile keyword, else a variable would have been cached ensuring that modified value is never read by another method, even when it is changed, but causing issue with the expected logic. To make things even more sure the standard instructions like lock can be used to ensure complete guarantee to the final solutionPernambuco
@MrinalKamboj: Can you describe exactly what sequence of reads and writes leads to s and t both being true? Suppose thread one wins the race and calls GetR before thread two calls SetR. q is true, then s is true. Then MethodTwo calls SetR and q is false, so t remains false. Similarly if thread two wins the race then it seems like t must be true but s must be false. My analysis here is incorrect. It is possible that s and t are both true. Where is the mistake?Landseer
@ Eric Lippert both s,t can true only in the case, when code executes such that it can execute the if conditions on either thread before variables r,q, which are evaluated in if, are reset on a different thread. Since processor finds that a variable being evaluated on a thread is modified on another one and it cannot be cached due to volatile directive, so it rearrange the sequence of calls that if (!GetR()) and if (!q) precedes SetR() and q = true. Otherwise if we see logically, as you have suggested it will always be one true and one false. Please let me know if am completely off the mark.Pernambuco
@Eric Lippert: x86 can swap reads/writes if they don't work on the same memory area. In case of volatile read, it is possible to move operations after the read and for volatile write, it is possible to move overations before the write. It means that 'q=true' and 'GetR()' can be swapped in MethodOne(), and if it happens, MethodTwo() can set 'r' before 'q' becomes true, causing both 's' and 't' becoming true.Pneumodynamics
@ViktorPeller: That's correct. The C# specification states that there is no guarantee that volatile reads and writes will be observed to have a consistent ordering across threads in the absence of locks, and in fact on x86, even with its strong memory model, we can observe that happening.Landseer
P
4

try this code, build release, run without Visual Studio:

class Foo
{
    private bool m_Done = false;

    public void A()
    {
        Task.Run(() => { m_Done = true; });
    }

    public void B()
    {
        for (; ; )
        {
            if (m_Done)
                break;
        }

        Console.WriteLine("finished...");
    }
}

class Program
{

    static void Main(string[] args)
    {
        var o = new Foo();
        o.A();
        o.B();

        Console.ReadKey();
    }
}

you have good chance to see it running forever

Pneumodynamics answered 6/6, 2015 at 2:5 Comment(2)
A good example, adding a volatile would indeed fix the issue, please correct me if I am wrongPernambuco
@Mrinal Kamboj yes, volatile would prevent caching the value of m_Done, which causes the main problem in the example.Pneumodynamics

© 2022 - 2024 — McMap. All rights reserved.