Самый быстрый способ безопасно прочитать содержимое long[], элементы которого изменяются одновременно

Когда у вас есть

long[] myArray = new long[256];

чьи элементы изменяются несколькими потоками с использованием

Interlocked.Increment(ref myArray[x])

точно невозможно получить моментальный снимок myArray в какой-то момент времени, так как одновременно выполняются незаблокированные записи, поэтому я не пытаюсь получить это.

Так что мне действительно нужно Volatile.Read каждого отдельного элемента, чтобы получить копию всех значений в какой-то момент в прошлом?

long[] copy = new long[256];
for (int i = 0; i < 256; i++)
    copy[i] = Volatile.Read(ref myArray[i]);

Поскольку меня не интересуют моментальные снимки в какой-то момент времени, устаревшие значения не являются проблемой, но поскольку 64-битные энергонезависимые операции чтения не являются атомарными, я боюсь, что следующее может дать мне до приращения половину long и половину после увеличения, что может дать значение, которого никогда не было в массиве.

long[] copy = new long[256];
for (int i = 0; i < 256; i++)
    copy[i] = myArray[i];

Так является ли вариант Volatile.Read правильным выбором, учитывая, что я не хочу использовать блокировку?


person Evgeniy Berezovsky    schedule 23.10.2018    source источник


Ответы (2)


Если устаревшие значения не являются для вас проблемой и все, что вам нужно, это атомарное чтение (не упорядоченное), то на x64 вы можете просто использовать обычное чтение вместо Volatile.Read. Это может быть полезно в системах ARM, где энергозависимые операции чтения/записи довольно тяжелые, поскольку они реализованы с помощью DMB.

Важно Согласно этому и это, вам нужно (собрать и) запустить вашу программу .Net в 64-битном режиме, чтобы это работало:

если вы используете код С# в 64-битной операционной системе в 64-битной версии CLR, то чтение и запись 64-битных двойных и длинных целых чисел также гарантированно будут атомарными

person OmariO    schedule 23.10.2018
comment
Это неожиданная, но очень приятная новость. У вас есть авторитетная ссылка на это? Справедливо ли это даже для 32-битных сборок на машинах x64? Я думаю, что это заслуживает собственного вопроса - person Evgeniy Berezovsky; 24.10.2018
comment
@EugeneBeresovsky: имейте в виду, что при таком подходе устаревшие значения могут означать значения, которые вообще никогда не обновляются после первого чтения (если у вас есть один поток чтения, который многократно считывает массив). Когда вы не используете какую-либо синхронизацию, компилятор может предположить, что текущий поток является единственным, который обращается к массиву, и поскольку текущий поток не записывает данные в массив, компилятор может предположить, что второй поток когда поток читает массив, значения не изменились, поэтому вы можете вечно видеть один и тот же снимок. - person Aasmund Eldhuset; 24.10.2018
comment
@AasmundEldhuset Это было бы практически устаревшим ... Однако я не верю, что существует какой-либо компилятор .net (java/c/c++), который анализирует, в каких потоках выполняются модификации, что даже невозможно статически для общий случай, а для времени выполнения накладные расходы на отслеживание, которые, вероятно, не стоили бы того в 99% случаев. У вас есть ссылка на ваше заявление? Может быть, даже пример из жизни? - person Evgeniy Berezovsky; 24.10.2018
comment
@EugeneBeresovsky: Верно; компилятор на самом деле не пытается определить, какой поток что делает, но, насколько я понимаю, ему разрешено компилировать любой код без синхронизации при условии, что его будет выполнять только один поток. У меня нет под рукой справочника по C#, но в Спецификации языка Java упоминается, как компилятор может выбрать повторно использовать значение, если в него не записываются записи. - person Aasmund Eldhuset; 24.10.2018
comment
@EugeneBeresovsky: для C# см. третий блок кода в msdn.microsoft.com/ en-us/magazine/jj883956.aspx для примера потока, который никогда не увидит значение, записанное другим потоком. По сути, если вы не синхронизируете должным образом, нет никаких гарантий относительно свежести данных. В зависимости от вашего компилятора и даже от каждого отдельного выполнения вашей программы (поскольку многие оптимизации выполняются JIT-компилятором), вы можете столкнуться с проблемами, а можете и не столкнуться, но если вы не синхронизируетесь должным образом, ваша программа не работает< /i> содержит ошибку, ожидающую своего появления. - person Aasmund Eldhuset; 24.10.2018
comment
Вот еще один пример, на этот раз из документации Golang: текст после третьего примера в разделе Неправильная синхронизация подтверждает мою заявление о том, что поток может никогда не увидеть значение, записанное другим потоком: Хуже того, нет никакой гарантии, что запись в done когда-либо будет наблюдаться main, поскольку между двумя потоками нет событий синхронизации. Как уже упоминалось, насколько я понимаю, в большинстве языков указано, что без явной синхронизации код может быть скомпилирован в предположении, что существует только один поток. - person Aasmund Eldhuset; 14.01.2020

В C# нет такой вещи, как тип atomic (как вы, наверное, знаете), есть только операции atomic.

Дрожание и/или Процессор могут решить изменить порядок инструкций, так что вы правы, предполагая, что вам нужно либо

  • Сериализировать доступ с помощью lock
  • Используйте класс Interlocked для записи (а в некоторых случаях и для чтения)
  • Объявите переменную volatile (хотя она недоступна для 64-битных типов и не применяется к массиву)
  • Или в вашей ситуации используйте Volatile.Read, если вы не возражаете против устаревших значений.

Чтобы ответить на ваш вопрос, и не видя вашего кода или того, как вы это делаете, ваш подход кажется правильным решением.

Метод Volatile.Read

Считывает значение поля. В системах, которые требуют этого, вставляет барьер памяти, который не позволяет процессору переупорядочивать операции с памятью следующим образом: если чтение или запись появляются после этого метода в коде, процессор не может переместить его перед этим методом.

person TheGeneral    schedule 23.10.2018
comment
Ваше третье предложение (объявить volatile) не относится к массивам - независимо от того, являются ли элементы 64-битными или нет. Потому что это будет охватывать только ссылку на массив, а не его содержимое. - person Evgeniy Berezovsky; 23.10.2018