Interlocked.CompareExchange инструкция пересылки начального значения - PullRequest
2 голосов
/ 14 мая 2019

Мне интересно, возможно ли, чтобы начальное значение в следующем коде могло быть переупорядочено, чтобы быть после вычисления, приводящего к неопределенному поведению.

Следующий пример взят из 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;
    }
}

Необходим ли барьер памяти между присвоением totalvalue начальному значению и фактическим вычислением?

Как я понимаю в настоящее время без барьера, его можно оптимизировать таким образом, чтобы удалить начальное значение, что приводит к проблемам безопасности потока, поскольку значение computedValue может быть рассчитано с использованием устаревшего значения, но 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 голосов
/ 14 мая 2019

ЦП никогда не «переупорядочивает» инструкции так, чтобы это могло повлиять на логику однопоточного исполнения.В случае, когда

initialValue = totalValue;
computedValue = initialValue + addend;

, вторая операция определенно зависит от значения, установленного в предыдущей операции.Процессор «понимает» это с точки зрения однопотоковой логики, поэтому эта последовательность никогда не будет переупорядочена.Однако следующие последовательности могут быть переупорядочены:

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
}

Что касается локальных переменных.Проблема переупорядочения - это проблема глобальной видимости, то есть видимости изменений, вносимых одним ядром / процессором в другие ядра / процессоры.Локальные переменные в основном имеют тенденцию быть видимыми только одним потоком (кроме некоторых редких сценариев, таких как замыкания, которые в случае воздействия вне метода фактически не являются локальными переменными), поэтому другие потоки не имеют доступа к ним итаким образом, другие ядра / процессоры не требуют их глобальной видимости.Другими словами, в большинстве случаев вам не нужно беспокоиться о переупорядочении операций с локальными переменными.

...