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