Interlocked.CompareExchange инструкция пересылки начального значения
Я задаюсь вопросом, возможно ли, что начальное значение в следующем коде может быть переупорядочено, чтобы быть после вычисления, приводящего к неопределенному поведению.
Следующий пример взят из 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 ответ
Процессор никогда не "переупорядочивает" инструкции так, чтобы это могло повлиять на логику однопоточного исполнения. В случае, когда
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
}
Что касается локальных переменных. Проблема переупорядочения - это проблема глобальной видимости, то есть видимости изменений, вносимых одним ядром / процессором в другие ядра / процессоры. Локальные переменные в основном имеют тенденцию быть видимыми только одним потоком (кроме некоторых редких сценариев, таких как замыкания, которые в случае воздействия вне метода фактически не являются локальными переменными), поэтому другие потоки не имеют доступа к ним и таким образом, другие ядра / процессоры не требуют их глобальной видимости. Другими словами, в большинстве случаев вам не нужно беспокоиться о переупорядочении операций с локальными переменными.