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

Есть справедливая доля вопросов о 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, но не нашел однозначного ответа (куча разных вопросов и комментариев / ответов в них все говорят противоречивые вещи).

2 ответа

Решение

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

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

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

Обидно, что документация того же уровня не существует в.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 на каждой итерации (так что вы получите свежее чтение). Таким образом, память выделяется только один раз.

Гарантирует ли 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 и никогда не увидите обновления.

Другие вопросы по тегам