Поля, считываемые / записываемые несколькими потоками, взаимосвязанные и изменчивые

Здесь, на SO, довольно много вопросов о Interlocked и volatile, я понимаю и знаю концепции volatile (без переупорядочивания, всегда чтение из памяти и т. Д.), И я знаю, как работает Interlocked в том смысле, что он выполняет атомарная операция.

Но мой вопрос таков: предположим, у меня есть поле, которое читается из нескольких потоков, это некоторый ссылочный тип, скажем: public Object MyObject;. Я знаю, что если я произведу сравнительный обмен на нем, например: Interlocked.CompareExchange(ref MyObject, newValue, oldValue), который заблокирован, гарантирует запись только newValue в то место памяти, на которое ссылается ref MyObject, если ref MyObject и oldValue в настоящее время относятся к одному и тому же объекту.

А как насчет чтения? Interlocked гарантирует, что любые потоки, читающие MyObject после успешной CompareExchange операции, мгновенно получат новое значение, или мне нужно пометить MyObject как volatile, чтобы гарантировать это?

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

[System.Diagnostics.DebuggerDisplay("Length={Length}")]
public class LinkedList<T>
{
    LList<T>.Cell head;

    // ....

    public void Prepend(T item)
    {
        LList<T>.Cell oldHead;
        LList<T>.Cell newHead;

        do
        {
            oldHead = head;
            newHead = LList<T>.Cons(item, oldHead);

        } while (!Object.ReferenceEquals(Interlocked.CompareExchange(ref head, newHead, oldHead), oldHead));
    }

    // ....
}

Теперь, после Prepend успеха, гарантированно ли потоки, читающие head, получат последнюю версию, даже если она не помечена как volatile?

Я проводил несколько эмпирических тестов, и, похоже, он работает нормально, и я искал здесь SO, но не нашел окончательного ответа (множество разных вопросов и комментариев / ответов в них все говорят о противоречивых вещах).


person thr    schedule 06.12.2011    source источник
comment
Это .NET, поэтому, насколько мне известно, он не должен зависеть от базовой модели памяти? Но для записи я использую x86-32 и x86-64, а не IA-64.   -  person thr    schedule 06.12.2011
comment
@thr: лежащий в основе mm имеет значение. Microsoft CLR предоставляет более строгие гарантии, чем спецификация ECMA CLI, поэтому, если вы хотите быть уверены, что ваш код будет работать на любой платформе, вы должны использовать только (более слабые) гарантии, предлагаемые спецификацией ECMA.   -  person LukeH    schedule 06.12.2011
comment
Модель памяти .NET уже обещает атомарность для объектных ссылок. Опубликованный код не может работать, он добавляет несколько копий элемента, когда другой поток также изменяет поле head. Отказ от использования lock для защиты head просто дает вам гарантированную гонку потоков. Использование барьера памяти не меняет этого, с таким же успехом можно его и опустить. Список распадается, когда другой поток удаляет головной узел.   -  person Hans Passant    schedule 06.12.2011
comment
@HansPassant: Нет, насколько я могу судить, этого не должно быть. Операция Interlocked завершится ошибкой, если head был изменен другим потоком. Цикл будет вращаться и повторить попытку.   -  person Brian Gideon    schedule 06.12.2011


Ответы (2)


Ваш код должен работать нормально. Хотя это четко не задокументировано, метод Interlocked.CompareExchange создаст барьер с полным забором. Я полагаю, вы могли бы внести одно небольшое изменение и опустить вызов Object.ReferenceEquals в пользу использования оператора !=, который по умолчанию выполнял бы ссылочное равенство.

Для чего стоит документация для Вызов Win API InterlockedCompareExchange намного лучше.

Эта функция создает полный барьер (или ограждение) памяти, чтобы гарантировать, что операции с памятью выполняются по порядку.

Жаль, что документация того же уровня не существует для аналога .NET BCL Interlocked. CompareExchange, потому что весьма вероятно, что они соответствуют одним и тем же базовым механизмам CAS.

Теперь, после успешного завершения Prepend, гарантированно ли головка чтения потоков получит последнюю версию, даже если она не помечена как изменчивая?

Нет, не обязательно. Если эти потоки не создают барьер, препятствующий получению, то нет гарантии, что они прочитают последнее значение. Убедитесь, что вы выполняете непостоянное чтение при любом использовании head. Вы уже убедились в этом в Prepend вызовом Interlocked.CompareExchange. Конечно, этот код может пройти цикл один раз с устаревшим значением head, но следующая итерация будет обновлена ​​из-за операции Interlocked.

Итак, если контекст вашего вопроса касался других потоков, которые также выполняют Prepend, то больше ничего делать не нужно.

Но если контекст вашего вопроса касался других потоков, выполняющих другой метод на LinkedList, убедитесь, что вы используете Thread.VolatileRead или Interlocked.CompareExchange, где это необходимо.

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

newHead = LList<T>.Cons(item, oldHead);

Единственная проблема, которую я вижу в этом, заключается в том, что память выделяется на каждой итерации цикла. В периоды высокой конкуренции цикл может повторяться несколько раз, прежде чем окончательно завершится успешно. Вы, вероятно, могли бы поднять эту строку за пределы цикла, если переназначаете связанную ссылку на oldHead на каждой итерации (так, чтобы вы получали новое чтение). Таким образом, память выделяется только один раз.

person Brian Gideon    schedule 06.12.2011
comment
Спасибо, наконец-то кто-то дал четкий ответ. Отмечено как правильное. Также спасибо за производительность опт. совет, хотя вы правы, что это сработает в моем небольшом примере, мой реальный код немного сложнее, и это не сработает. - person thr; 07.12.2011

Гарантирует ли Interlocked, что любые потоки, читающие MyObject после успешной операции CompareExchange, мгновенно получат новое значение, или мне нужно пометить MyObject как изменчивый, чтобы гарантировать это?

Да, последующие чтения в том же потоке получат новое значение.

Ваш цикл разворачивается до этого:

oldHead = head;
newHead = ... ;

Interlocked.CompareExchange(ref head, newHead, oldHead) // full fence

oldHead = head; // this read cannot move before the fence

ИЗМЕНИТЬ:

Нормальное кеширование может происходить и в других потоках. Учитывать:

var copy = head;

while ( copy == head )
{
}

Если вы запустите это в другом потоке, компилятор может кэшировать значение head и никогда не увидеть обновление.

person Nick Butler    schedule 06.12.2011
comment
Не пытаюсь показаться грубым, но это был не мой вопрос. Я спросил о любых ветках, а не о той же ветке. - person thr; 06.12.2011
comment
Вы на 100% уверены в этом (ваше изменение), я видел противоречивую информацию об этом (некоторые люди говорят, что вы делаете, некоторые люди говорят разные вещи и т. Д.), И если вы посмотрите исходный код Mono, их параллельные коллекции не использовать изменчивые поля, которые, как я полагаю, необходимы? - person thr; 06.12.2011
comment
Хотя я предполагаю, что, поскольку сравнения для проверки правильности операции всегда выполняются с помощью Interlocked.CompareExchange ‹T› () (сейчас речь идет о моно параллельных коллекциях), это заставит их всегда проверять последнее значение и в случае, если оно устарело, их процессор кеши будут обновлены. - person thr; 06.12.2011
comment
После ограждения кеши в процессоре будут согласованными, поэтому любая фактическая инструкция чтения, выполненная на любом ядре, увидит новое значение. Однако компилятор теоретически может полностью оптимизировать чтение. - person Nick Butler; 06.12.2011
comment
Николас: Отлично, это был ответ, который я искал - person thr; 06.12.2011
comment
@thr Да, CompareExchange всегда будет читать последнее значение - person Nick Butler; 06.12.2011