3

I've always been told to puts locks around variables that multiple threads will access, I've always assumed that this was because you want to make sure that the value you are working with doesn't change before you write it back i.e.

mutex.lock()
int a = sharedVar
a = someComplexOperation(a)
sharedVar = a
mutex.unlock()

And that makes sense that you would lock that. But in other cases I don't understand why I can't get away with not using Mutexes.

Thread A:

sharedVar = someFunction()

Thread B:

localVar = sharedVar

What could possibly go wrong in this instance? Especially if I don't care that Thread B reads any particular value that Thread A assigns.

Whyrusleeping
  • 889
  • 2
  • 9
  • 20
  • I am not an authority, but I am guessing it is because you could end up with collisions. if you are trying to access a variable the same time it is being written to, then you are probably going to get an access violation. – Pow-Ian Dec 13 '12 at 21:08
  • @Pow-Ian memory access is always serialisable. The hardware is responsible for handling this in a reasonable way. – John Dvorak Dec 13 '12 at 21:09
  • Problem is that a variable may contain a value you are not expecting. When you are only reading it's not a problem. When you start writing and using the variable, you can no longer make correct decisions bases on the value of the variable. – dmaij Dec 13 '12 at 21:09
  • @Jan Dvorak thanks, that is good to know. – Pow-Ian Dec 13 '12 at 21:11
  • 1
    You'll want to read about *tearing* and *sequential consistency*. – Ben Voigt Dec 13 '12 at 21:35

4 Answers4

5

It depends a lot on the type of sharedVar, the language you're using, any framework, and the platform. In many cases, it's possible that assigning a single value to sharedVar may take more than one instruction, in which case you may read a "half-set" copy of the value.

Even when that's not the case, and the assignment is atomic, you may not see the latest value without a memory barrier in place.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • It's not just the type. It also depends on the language (some have only reference types, where assignment is nigh-universally atomic) and the memory model (which may guarantee atomicity for some or all types). –  Dec 13 '12 at 21:10
  • @delnan Yes - type/language/framework/underlying hardware/etc - it all matters. – Reed Copsey Dec 13 '12 at 21:10
  • It's useful to know the term for observing a partially-updated value: *tearing*. And alignment is vitally important when trying to coax the hardware to perform updates atomically. – Ben Voigt Dec 13 '12 at 21:14
  • 1
    Is there actually a language that does not guarantee an atomic access to reference-type variables? – John Dvorak Dec 13 '12 at 21:15
  • So what about in the case of a simple float in c++, would I ever run into a problem? – Whyrusleeping Dec 13 '12 at 21:19
  • @Whyrusleeping: Yes, because absent memory barriers writes may be published out of order, and reads may be performed out of order also, creating "impossible" combinations. Reordering can be done by both the compiler and the CPU hardware; `volatile` restricts the compiler reordering only. To prevent reordering altogether requires a memory barrier as Reed mentioned. – Ben Voigt Dec 13 '12 at 21:22
  • @BenVoigt what do you mean by 'impossible' combinations? – Whyrusleeping Dec 13 '12 at 21:24
  • `a = 0; b = 0; start thread { a = 1; b = 2; }; start thread { x = b; y = a; }; join all threads; print a, b, x, y;` Would it surprise you to see `1 2 2 0`? – Ben Voigt Dec 13 '12 at 21:26
  • @JanDvorak Some older languages don't specify at all, which means there are no guarantees. For example, nothing in the C specification guarantees that pointer assignment is atomic - it's up to the platform/compiler. – Reed Copsey Dec 13 '12 at 21:27
  • 1
    @Whyrusleeping I believe a "float" in C++ depends on the platform/compiler ;) On some (mostly older) embedded systems, a float assignment is not atomic, I believe. – Reed Copsey Dec 13 '12 at 21:27
  • @JanDvorak Interesting reading material here: http://stackoverflow.com/questions/879077/is-changing-a-pointer-considered-an-atomic-action-in-c – Reed Copsey Dec 13 '12 at 21:29
  • @JanDvorak BTW - there's a reason methods like InterlockedExchangePointer are necessary: http://msdn.microsoft.com/en-us/library/windows/desktop/ms683609(v=vs.85).aspx – Reed Copsey Dec 13 '12 at 21:30
  • @Reed: Of course, interlocked-exchange is more than just a read or write. – Ben Voigt Dec 13 '12 at 21:33
  • @BenVoigt what if my program doesn't care that it gets 2 0 as opposed to 1 2? – Whyrusleeping Dec 13 '12 at 21:43
  • @Whyrusleeping: I don't think you looked at that closely enough. You'd expect `1 2 0 0` or `1 2 0 1` or `1 2 2 1`, which are all sequentially consistent. Are you prepared to deal with lack of sequential consistency and results such as `1 2 2 0`? – Ben Voigt Dec 14 '12 at 14:12
4

MSDN Magazine has a good explanation of different problems you may encounter in multithreaded code:

  • Forgotten Synchronization
  • Incorrect Granularity
  • Read and Write Tearing
  • Lock-Free Reordering
  • Lock Convoys
  • Two-Step Dance
  • Priority Inversion

The code in your question is particularly vulnerable to Read/Write Tearing. But your code, having neither locks nor memory barriers, is also subject to Lock-Free Reordering (which may include speculative writes in which thread B reads a value that thread A never stored) in which side-effects become visible to a second thread in a different order from how they appeared in your source code.

It goes on to describe some known design patterns which avoid these problems:

  • Immutability
  • Purity
  • Isolation

The article is available here

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
2

The main problem is that the assignment operator (operator= in C++) is not always guaranteed to be atomic (not even for primitive, built in types). In plain English, that means that assignment can take more than a single clock cycle to complete. If, in the middle of that, the thread gets interrupted, then the current value of the variable might be corrupted.

Let me build off of your example:

Lets say sharedVar is some object with operator= defined as this:

object& operator=(const object& other) {
    ready = false;
    doStuff(other);
    if (other.value == true) {
        value = true;
        doOtherStuff();
    } else {
        value = false;
    }
    ready = true;
    return *this;
}

If thread A from your example is interrupted in the middle of this function, ready will still be false when thread B starts to run. This could mean that the object is only partially copied over, or is in some intermediate, invalid state when thread B attempts to copy it into a local variable.

For a particularly nasty example of this, think of a data structure with a removed node being deleted, then interrupted before it could be set to NULL.

(For some more information regarding structures that don't need a lock (aka, are atomic), here is another question that talks a bit more about that.)

Community
  • 1
  • 1
Tyler Gill
  • 861
  • 6
  • 9
  • Is this a problem even in the case of simple types, like floats? (if shared var is a float) – Whyrusleeping Dec 13 '12 at 21:21
  • With some simple types, yes, this is still a problem. C++11 introduces the std::atomic<> wrapper class, that will provide guaranteed atomic access to many basic types, but for the most part, you can't always assume something that looks atomic will be atomic unless the compiler guarantees that to be the case. [Here](http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=469) is one article I found that talks about a few operators that are or are not atomic under MSVC, but the main take-away is that basic integer math is usually atomic, but other things might not be. – Tyler Gill Dec 13 '12 at 21:32
  • [Here](http://stackoverflow.com/questions/1292786/is-updating-double-operation-atomic) is another question that seems to have a bit more detailed information specifically about floating point types. – Tyler Gill Dec 13 '12 at 21:36
0

This could go wrong, because threads can be suspended and resumed by the thread scheduler, so you can't be sure about the order these instructions are executed. It might just as well be in this order:

Thread B:

localVar = sharedVar

Thread A:

sharedVar = someFunction()

In which case localvar will be null or 0 (or some completeley unexpected value in an unsafe language), probably not what you intended.

Mutexes actually won't fix this particular issue by the way. The example you supply does not lend itself well for parallelization.

gjvdkamp
  • 9,929
  • 3
  • 38
  • 46