Interlocked.CompareExchange指令重新初始化初始值

时间:2019-05-14 08:56:17

标签: c# thread-safety memory-barriers interlocked instruction-reordering

我想知道是否可以将以下代码中的初始值重新排序为在计算之后导致不确定的行为。

以下示例摘自https://docs.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;
    }
}

在将总值辅助为初始值与实际计算之间是否需要存储障碍?

据我目前的理解,没有障碍,可以通过消除导致线程安全问题的初始值的方式对其进行优化,因为可以使用陈旧的值来计算calculatedValue,但是CompareExchange将不再检测到此值:

    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;
    }

这里是否缺少局部变量的特殊规则,可以解释为什么该示例不使用内存屏障?

1 个答案:

答案 0 :(得分:0)

CPU永远不会以可能影响单线程执行逻辑的方式对指令进行“重新排序”。以防万一

initialValue = totalValue;
computedValue = initialValue + addend;

第二个操作肯定取决于上一个操作中设置的值。 CPU从其单线程逻辑角度“理解”了这一点,因此该序列永远不会重新排序。但是,以下序列可以重新排序:

initialValue = totalValue;
anotherValue = totalValue;

varToInitialize = someVal;
initialized = true;

如您所见,单核执行不会受到影响,但是在多核上这可能会带来一些问题。例如,如果我们围绕以下事实建立逻辑,即如果变量initialized设置为true,那么varToInitialize应该用一些值初始化,那么在多核环境中我们可能会遇到麻烦:

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

至于局部变量。重新排序的问题是全局可见性的问题,即一个内核/ CPU对其他内核/ CPU所做的更改的可见性。局部变量主要倾向于仅在单个线程中可见(除了某些罕见的情况,例如闭包,在方法外公开的情况下,闭包实际上不是局部变量),因此其他线程无法访问它们,并且因此,其他内核/ CPU不需要其全局可见性。因此,换句话说,在大多数情况下,您无需担心局部变量操作的重新排序。