Поля, считываемые / записываемые несколькими потоками, блокируются или изменяются - PullRequest
9 голосов
/ 06 декабря 2011

Здесь довольно много вопросов о Interlocked против volatile здесь, на SO, я понимаю и знаю концепции 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 ]

6 голосов
/ 06 декабря 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 и никогда не видеть обновления.

4 голосов
/ 06 декабря 2011

Ваш код должен работать нормально. Хотя это не ясно задокументировано, метод 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 (чтобы вы получили новое чтение). Таким образом, память выделяется только один раз.

...