cconcurrencymutexrace-conditionprocedural-programming

Do all getters, that return a variable potentially modified by a different thread, need to be protected with mutex or equivalent?


Let's suppose the following C code snippet:

static bool var;

DECLARE_MUTEX(lock);

void mod_set_var(bool v)
{
  mutex_lock(lock);
  var = v;
  mutex_unlock(lock);
}

bool mod_get_var(void)
{
  return var;
}

Let's suppose that the above functions can be accessed by many different threads, and that we don't want to rely at all in the ability of the underneath arch. to do atomic assignments and the like

As far as I understand, the theory says that mod_get_var should be protected with a mutex. However, I can't imagine the case in which the above code may cause a problem.

I have read this other post in which @David Schwartz exposes an example of disaster. However, that happens if foo j; is created locally inside a function, but here var is a statically allocated variable. I'm not sure if the mentioned example applies to the case above. If it does, I don't see how.

How can the code above end up in undesired/unexpected behavior?


Solution

  • Do all getters that return a variable potentially modified by a different thread be protected with mutex or equivalent?

    Yes. If multiple threads access the same non-atomic object and at least one of those accesses is a write, then all the accesses must be synchronized, not just the writes, else there is a data race. A program that contains a data race has undefined behavior.

    However, I can't imagine the case in which the above code may cause a problem.

    Do you want to bet the reliability of your program on the breadth of your imagination? Why spend effort thinking about what you might be able to get away with when you already know what the right thing to do is? In particular, undefined behavior is undefined -- you don't get to limit the scope of its manifestation.

    How can the code above end up in undesired/unexpected behavior?

    Easily.

    Consider this:

    void busy_wait() {
        while (mod_get_var()) ;
    }
    

    Now imagine that the compiler inlines the mod_get_var() call to a direct read of var. That read is not synchronized, so the compiler is not obliged to account for the possibility that different reads will observe different values. Thus, it might optimize the function to the equivalent of

    void busy_wait() {
        if (var) while(1) ;
    }
    

    If you were counting on a different thread to signal threads to break out of busy_wait() then you could be in for a rude surprise.