Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# variable freshness

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.

like image 490
Andre Eid Avatar asked Jun 06 '15 00:06

Andre Eid


2 Answers

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.

like image 59
Eric Lippert Avatar answered Sep 30 '22 01:09

Eric Lippert


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

like image 39
Viktor Peller Avatar answered Sep 30 '22 01:09

Viktor Peller