Search code examples
c#thread-safetymemory-barriersinterlockedinstruction-reordering

Interlocked.CompareExchange instruction reodering of the initialvalue


Iam wondering if its possible that the initialvalue in the following code can be reordered to be after the computation resulting in undefined behavior.

The following example is taken from https://learn.microsoft.com/en-us/dotnet/api/system.threading.interlocked.compareexchange?view=netframework-4.8

public class ThreadSafe
{
    // Field totalValue contains a running total that can be updated
    // by multiple threads. It must be protected from unsynchronized 
    // access.
    private float totalValue = 0.0F;

    // The Total property returns the running total.
    public float Total { get { return totalValue; }}

    // AddToTotal safely adds a value to the running total.
    public float AddToTotal(float addend)
    {
        float initialValue, computedValue;
        do
        {
            // Save the current running total in a local variable.
            initialValue = totalValue;
            //Do we need a memory barrier here??
            // Add the new value to the running total.
            computedValue = initialValue + addend;

            // CompareExchange compares totalValue to initialValue. If
            // they are not equal, then another thread has updated the
            // running total since this loop started. CompareExchange
            // does not update totalValue. CompareExchange returns the
            // contents of totalValue, which do not equal initialValue,
            // so the loop executes again.
        }
        while (initialValue != Interlocked.CompareExchange(ref totalValue, 
            computedValue, initialValue));
        // If no other thread updated the running total, then 
        // totalValue and initialValue are equal when CompareExchange
        // compares them, and computedValue is stored in totalValue.
        // CompareExchange returns the value that was in totalValue
        // before the update, which is equal to initialValue, so the 
        // loop ends.

        // The function returns computedValue, not totalValue, because
        // totalValue could be changed by another thread between
        // the time the loop ends and the function returns.
        return computedValue;
    }
}

Is a memory barrier needed between assiging totalvalue to initialvalue and the actual computation?

As I currently understand without a barrier it could be optimized in a way that removes the initialvalue resulting in thread safety issues because the computedValue is could be calculated with a stale value but the CompareExchange will no longer detect this:

    public float AddToTotal(float addend)
    {
        float computedValue;
        do
        {
            // Add the new value to the running total.
            computedValue = totalValue + addend;

            // CompareExchange compares totalValue to initialValue. If
            // they are not equal, then another thread has updated the
            // running total since this loop started. CompareExchange
            // does not update totalValue. CompareExchange returns the
            // contents of totalValue, which do not equal initialValue,
            // so the loop executes again.
        }
        while (totalValue != Interlocked.CompareExchange(ref totalValue, 
            computedValue, totalValue));
        // If no other thread updated the running total, then 
        // totalValue and initialValue are equal when CompareExchange
        // compares them, and computedValue is stored in totalValue.
        // CompareExchange returns the value that was in totalValue
        // before the update, which is equal to initialValue, so the 
        // loop ends.

        // The function returns computedValue, not totalValue, because
        // totalValue could be changed by another thread between
        // the time the loop ends and the function returns.
        return computedValue;
    }

Are there special rules for local variables iam missing here that explains why the example does not use a memory barrier?


Solution

  • A CPU never "reorders" instructions in way which may affect logic of a single-threaded execution. In case when

    initialValue = totalValue;
    computedValue = initialValue + addend;
    

    the second operation is definitely depends on value set in the previous operation. A CPU "understands" this from its single-threaded logic perspective, so this sequence never will be reordered. However the following sequences can be reordered:

    initialValue = totalValue;
    anotherValue = totalValue;
    

    or

    varToInitialize = someVal;
    initialized = true;
    

    As you can see the single-core execution won't be affected but on multiple cores this can impose some problems. For example if we build our logic around the fact that if variable initialized is set to true then the varToInitialize should be initialized with some value we can be in trouble on multi-core environment:

    if (initialized)
    {
        var storageForVal = varToInitialize; // can still be not initalized
        ...
        // do something with storageForVal with assumption that we have correct value
    }
    

    As for the local variables. The problem of reordering is problem of global visibility, that is visibility of changes made by one core/CPU to other cores/CPUs. The local variables are mainly tend to be visible only by single thread (apart from some rare scenarios like closures which in the case of exposure outside of a method effectively aren't local variables) so other threads don't have an access to them and thus other cores/CPUs don't require their global visibility. So in other words in vast majority of cases you don't need to worry about local variable operations reordering.