Должен ли потокобезопасный класс иметь в конце конструктора барьер памяти?

При реализации класса, который должен быть потокобезопасным, должен ли я включать барьер памяти в конце его конструктора, чтобы гарантировать, что все внутренние структуры будут инициализированы до того, как к ним можно будет получить доступ? Или это ответственность потребителя - вставить барьер памяти перед тем, как сделать экземпляр доступным для других потоков?

Упрощенный вопрос:

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

ConcurrentQueue<int> queue = null;

Parallel.Invoke(
    () => queue = new ConcurrentQueue<int>(),
    () => queue?.Enqueue(5));

Обратите внимание, что для программы допустимо ничего не ставить в очередь, как это могло бы случиться, если бы второй делегат выполнялся раньше первого. (Нулевой условный оператор ?. защищает от NullReferenceException здесь.) Однако для программы не должно быть приемлемым выдавать IndexOutOfRangeException, NullReferenceException, ставить в очередь 5 несколько раз, застревать в бесконечном цикле или выполнять какие-либо другие странные действия. вещи, вызванные расовыми опасностями на внутренних конструкциях.

Подробный вопрос:

Конкретно представьте, что я реализую простую потокобезопасную оболочку для очереди. (Я знаю, что .NET уже предоставляет ConcurrentQueue<T> < / a>; это просто пример.) Я мог бы написать:

public class ThreadSafeQueue<T>
{
    private readonly Queue<T> _queue;

    public ThreadSafeQueue()
    {
        _queue = new Queue<T>();

        // Thread.MemoryBarrier(); // Is this line required?
    }

    public void Enqueue(T item)
    {
        lock (_queue)
        {
            _queue.Enqueue(item);
        }
    }

    public bool TryDequeue(out T item)
    {
        lock (_queue)
        {
            if (_queue.Count == 0)
            {
                item = default(T);
                return false;
            }

            item = _queue.Dequeue();
            return true;
        }
    }
}

После инициализации эта реализация является поточно-ориентированной. Однако, если сама инициализация выполняется другим потоком-потребителем, может возникнуть опасность гонки, в результате чего последний поток получит доступ к экземпляру до того, как будет инициализирован внутренний Queue<T>. В качестве надуманного примера:

ThreadSafeQueue<int> queue = null;

Parallel.For(0, 10000, i =>
{
    if (i == 0)
        queue = new ThreadSafeQueue<int>();
    else if (i % 2 == 0)
        queue?.Enqueue(i);
    else
    {
        int item = -1;
        if (queue?.TryDequeue(out item) == true)
            Console.WriteLine(item);
    }
});

Допускается пропуск некоторых чисел в приведенном выше коде; однако без барьера памяти он также может получить NullReferenceException (или другой странный результат) из-за того, что внутренний Queue<T> не был инициализирован к моменту вызова Enqueue или TryDequeue.

Обязан ли потокобезопасный класс включать барьер памяти в конце своего конструктора, или это потребитель должен включить барьер памяти между созданием экземпляра класса и его видимостью для других потоков? Каково соглашение .NET Framework для классов, помеченных как потокобезопасные?

Изменить: это сложная тема для обсуждения, поэтому я понимаю путаницу в некоторых комментариях. Экземпляр может выглядеть недоделанным, если к нему обращаются из других потоков без надлежащей синхронизации. Эта тема широко обсуждается в контексте блокировки с двойной проверкой, которая нарушается в соответствии со спецификацией интерфейса командной строки ECMA без использования барьеров памяти (например, через volatile). Согласно Джону Скиту:

Модель памяти Java не гарантирует, что конструктор завершит работу до того, как ссылка на новый объект будет присвоена экземпляру. Модель памяти Java подверглась переработке для версии 1.5, но после этого блокировка с двойной проверкой все еще не работает без изменчивой переменной (как в C #).

Без каких-либо барьеров памяти, он также сломан в спецификации ECMA CLI. Возможно, что в рамках модели памяти .NET 2.0 (которая сильнее, чем спецификация ECMA) это безопасно, но я бы не стал полагаться на эту более сильную семантику, особенно если есть какие-либо сомнения в безопасности.


person Douglas    schedule 10.08.2016    source источник
comment
Исходный код для ConcurrentQueue<T>, который вы упомянули, не имеет никакой защиты в своем конструкторе. Сделайте из этого что хочешь. referenceource.microsoft.com/#mscorlib/system/Collections.com/#mscorlib/system/Collections   -  person Bradley Uffner    schedule 10.08.2016
comment
Как насчет инициализации потребителя с помощью Lazy ‹T›, который делает инициализацию потокобезопасной? :)   -  person Zein Makki    schedule 10.08.2016
comment
Если в конструкторе есть асинхронные вызовы, можно ли установить ссылку на экземпляр до его создания?   -  person Uueerdo    schedule 10.08.2016
comment
@Uueerdo Как видно из единственного потока, нет. Как видно из нескольких потоков, да.   -  person Servy    schedule 10.08.2016
comment
@BradleyUffner: Хороший вопрос. Это делает вопрос более ясным.   -  person Douglas    schedule 10.08.2016
comment
Как именно вызывающий может сделать экземпляр доступным для других потоков до его создания, если сам вызывающий еще не имеет доступа к экземпляру ?. Единственный способ сделать экземпляр доступным до его создания - это если конструктор вызывает некоторый внешний код, передающий this.   -  person Ivan Stoev    schedule 10.08.2016
comment
@ user3185569: Это было бы хорошим решением, если существует опасность гонки. Мой вопрос в том, нужен ли Lazy<T> или должен ли потокобезопасный класс обеспечивать эту защиту сам.   -  person Douglas    schedule 10.08.2016
comment
Я думаю, это может быть проблема только с static конструкторами (или конструкторами, которые разделяют static состояние). Конструкторы экземпляров обычно кажутся безопасными.   -  person Bradley Uffner    schedule 10.08.2016
comment
@Douglas моя интерпретация всегда заключалась в том, что ресурс является потокобезопасным, но у меня нет ресурса до тех пор, пока он не будет инициализирован, поэтому это задание вызывающих абонентов для защиты инициализации   -  person HasaniH    schedule 10.08.2016
comment
@IvanStoev В однопоточном контексте, да, в многопоточном контексте вы можете заметить, что порядок операций отличается от гарантий для однопоточной программы. Вашему процессору разрешено переупорядочивать разные записи на совершенно разные значения, которые не зависят друг от друга.   -  person Servy    schedule 10.08.2016
comment
@Servy Я знаю это в целом, но не могли бы вы предоставить ссылку или что-то, что касается конструкторов? Все, что я вижу, это . Это явным образом не является требованием, чтобы соответствующая реализация интерфейса командной строки гарантировала, что все обновления состояния, выполняемые в конструкторе, должны быть единообразно видимыми до завершения конструктора. Генераторы CIL могут сами обеспечить выполнение этого требования, вставляя соответствующие вызовы в барьер памяти или изменяемые инструкции записи. Но речь идет о до завершения конструктора.   -  person Ivan Stoev    schedule 10.08.2016
comment
@IvanStoev В спецификации указано, что требуется; они не предоставляют исчерпывающий список всего того, что им не требуется. В спецификациях не говорится, что конструктору разрешено возвращать объект до того, как конструктор завершит свою работу, скорее, значение, возвращаемое конструктором, не входит конкретно в список операций, которые разные потоки гарантированно будут соблюдать в логически согласованном порядке. в однопоточную программу.   -  person Servy    schedule 10.08.2016
comment
@Servy, значит, вы говорите, что присвоение может произойти после выделения, но до построения / инициализации, даже если все это происходит в одном потоке?   -  person Uueerdo    schedule 10.08.2016
comment
@IvanStoev: Учитывая оператор _queue = new Queue<T>();, модель компилятора / архитектуры / памяти может привести к тому, что область памяти нового объекта будет казаться назначенной _queue до завершения конструктора Queue<T>().   -  person Douglas    schedule 10.08.2016
comment
@Uueerdo Однопоточная программа не могла наблюдать, как эти действия происходят не по порядку. Другой поток, наблюдающий за действиями, выполняемыми над другим, имеет радикально меньше ограничений на наблюдаемый порядок операций. Что касается этого примера, второй поток может фактически наблюдать вызов конструктора из другого потока, возвращающего экземпляр, до того, как сам конструктор завершит работу. Поток, вызвавший конструктор, не может наблюдать этот необычный порядок, но любой другой поток может.   -  person Servy    schedule 10.08.2016
comment
@Douglas Я поддержал ваш вопрос, потому что считаю его интересным. Но если язык высокого уровня, такой как C #, не может предоставить такой простой гарантии, то я не понимаю, что мы здесь делаем. Я бросил программировать :)   -  person Ivan Stoev    schedule 10.08.2016
comment
@IvanStoev: Вы далеко не одиноки в своем разочаровании. Подавляющее большинство согласны с тем, что большинству программистов следует избегать низкоуровневых методов синхронизации и просто полагаться на lock или высокоуровневые фреймворки, такие как TPL.   -  person Douglas    schedule 10.08.2016
comment
@Theodor Zoulias На x86 нельзя переупорядочивать хранилища относительно друг друга, поэтому, если вы видите результат конструктора, вы также видите значение _queue. На ARM64 все ставки сняты. Я предполагаю, что JIT-компилятор вставит барьер, чтобы избежать нарушения существующей программы, но, к сожалению, я не знаю, как это проверить (у меня нет процессора ARM64, а Sharplab поддерживает только x86)   -  person Kevin Gosse    schedule 03.01.2021


Ответы (4)


Lazy<T> - очень хороший выбор для потоковой инициализации. Я считаю, что потребителю следует предоставить следующее:

var queue = new Lazy<ThreadSafeQueue<int>>(() => new ThreadSafeQueue<int>());

Parallel.For(0, 10000, i =>
{

    else if (i % 2 == 0)
        queue.Value.Enqueue(i);
    else
    {
        int item = -1;
        if (queue.Value.TryDequeue(out item) == true)
            Console.WriteLine(item);
    }
});
person Zein Makki    schedule 10.08.2016
comment
+1: Это то, чем я сейчас занимаюсь, учитывая мою неуверенность. Но я хотел бы знать, следует ли ожидать, что код будет работать без синхронизации потока от потребителя. Другими словами: если бы я был тем, кто реализовал потокобезопасный класс, мог бы я назвать свой класс потокобезопасным, если потребителям все еще нужно было использовать Lazy<T>? - person Douglas; 10.08.2016
comment
Я не понимаю редактирования. Мой исходный код никогда бы не выполнил инициализацию дважды. - person Douglas; 10.08.2016
comment
@Douglas У вас действительно есть очередь null при запуске этого кода? Хотя бы один раз ? - person Zein Makki; 10.08.2016
comment
@ user3185569 Код никогда не может печатать null, но он может бросать элементы на пол, если они обрабатываются до инициализации очереди. Вы предполагаете, что 0-я итерация завершается до начала любой другой итерации. Такой гарантии нет. - person Servy; 10.08.2016
comment
@ user3185569: Возможно, я выбрал плохой пример. Код может отбрасывать некоторые числа. Недопустимо, чтобы код бросал NullReferenceException, IndexOutOfRangeException, печатал повторяющиеся числа, застревал в бесконечных циклах или делал какие-либо другие странные вещи, вызванные опасностями расы во внутренних структурах. - person Douglas; 10.08.2016

Должен ли потокобезопасный класс иметь в конце конструктора барьер памяти?

Я не вижу для этого причины. queue - это локальная переменная, которая назначается из одного потока и доступна из другого. Такой одновременный доступ должен быть синхронизирован, и за это отвечает код доступа. Это не имеет ничего общего с конструктором или типом переменной, такой доступ всегда должен быть явно синхронизирован, иначе вы попадете в опасную область даже для примитивных типов (даже если присвоение является атомарным, вы можете попасть в ловушку кеша). Если доступ к переменной правильно синхронизирован, он не нуждается в поддержке в конструкторе.

person Antonín Lejsek    schedule 01.01.2021
comment
Не говоря уже о том, что этот ответ неверен, я считаю, что он выходит за рамки заданного вопроса. Этот вопрос касается классов, а не примитивных типов (которые обычно являются структурами). В этом контексте атомарность присвоения следует рассматривать как факт, а не как нечто сомнительное. - person Theodor Zoulias; 01.01.2021

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

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

  2. Добавление строки Thread.MemoryBarrier(); в конце конструктора недостаточно, чтобы сделать конструктор потокобезопасным, потому что этот оператор влияет только на поток, который запускает конструктор¹. Другие потоки, которые могут одновременно обращаться к находящемуся в стадии разработки экземпляру, не затрагиваются. Видимость памяти является кооперативной, и один поток не может изменить то, что видит другой поток, изменяя поток выполнения другого потока (или делая недействительным локальный кеш ядра ЦП, на котором работает другой поток) некооперативным образом.

  3. Правильный и надежный способ гарантировать, что все потоки видят экземпляр, имеющий допустимое состояние, - это включить надлежащие барьеры памяти во все потоки. Этого можно добиться, объявив экземпляр как volatile < / a>, если это поле класса, или иным образом с использованием методов статического _ 3_ класс:

ThreadSafeQueue<int> queue = null;

Parallel.For(0, 10000, i =>
{
    if (i == 0)
        Volatile.Write(ref queue, new ThreadSafeQueue<int>());
    else if (i % 2 == 0)
        Volatile.Read(ref queue)?.Enqueue(i);
    else
    {
        int item = -1;
        if (Volatile.Read(ref queue)?.TryDequeue(out item) == true)
            Console.WriteLine(item);
    }
});

В этом конкретном примере было бы проще и эффективнее создать экземпляр переменной queue перед вызовом метода Parallel.For. Это сделает ненужными явные Volatile вызовы. Метод Parallel.For использует Tasks внутри, а TPL включает соответствующие барьеры памяти в начале / конце каждой задачи. Барьеры памяти неявно и автоматически создаются инфраструктурой .NET с помощью любого встроенного механизма, который запускает поток или вызывает выполнение делегата в другом потоке. (цитирование)

Повторюсь, я не уверен на 100% в правильности представленной выше информации.

¹ Цитата из документации метода Thread.MemoryBarrier: Синхронизирует доступ к памяти следующим образом: процессор, выполняющий текущий поток, не может переупорядочить инструкции таким образом, чтобы обращения к памяти до вызова MemoryBarrier() выполнялись после обращений к памяти, следующих за вызовом _12 _.

person Theodor Zoulias    schedule 01.01.2021
comment
Добавление строки Thread.MemoryBarrier(); в конце конструктора недостаточно, чтобы сделать конструктор поточно-ориентированным. ‹- Это не обязательно верно для потоковобезопасных классов, подобных тому, что я представил. Да, видимость памяти является кооперативной, но все другие методы класса уже обеспечивают это с помощью своих операторов lock, которые создают неявный барьер памяти. - person Douglas; 04.01.2021
comment
@Douglas, ваш ThreadSafeQueue<T> класс создает Queue<T> в своем конструкторе. Конструктор класса Queue<T> не работает много. Он просто назначает статическое поле T[] _emptyArray как значение частного поля T[] _array. Но насколько вы уверены в том, что другой поток, которому передается ссылка на вновь созданный ThreadSafeQueue<T>, не увидит _array, имеющего исходное значение null, что вызовет NullReferenceException? - person Theodor Zoulias; 04.01.2021
comment
Единственный способ доступа к _array - это методы Enqueue или TryDequeue, оба из которых принимают lock (и, следовательно, неявный барьер памяти) перед доступом к экземпляру _array. Тем не менее, я не уверен, защищает ли неявный барьер от того, что x в lock(x) все еще остается null, так что вы правы, если это то, что вы имели в виду. - person Douglas; 05.01.2021
comment
@Douglas хорошее замечание о барьере памяти, неявно вставленном lock. Я не думал об этом. Это может быть ингредиент, обеспечивающий безопасный доступ к энергонезависимой переменной / полю, в котором хранятся экземпляры вашего класса. А может и нет. Надеюсь, какой-нибудь эксперт, который изучил спецификации CLR / C # и знает об этом, сможет нас просветить! - person Theodor Zoulias; 05.01.2021
comment
Я изучал эту тему некоторое время и пришел к выводу, что модель памяти в спецификации ECMA CLI нарушена. Однако CLR и все другие основные реализации среды выполнения предлагают более сильную модель памяти, чем требуется спецификацией, что позволяет избежать этих ловушек. - person Douglas; 05.01.2021
comment
@ Дуглас не рад услышать об этом заключении! Лично я принял следующий совет Игоря Островского: Весь код, который вы пишете, должен полагаться только на гарантии, предоставляемые спецификацией ECMA C #, а не на какие-либо детали реализации, описанные в этой статье. From эту статью. Но если спецификация нарушена, мне, возможно, придется пересмотреть. - person Theodor Zoulias; 05.01.2021
comment
Я не читал Игоря, но многие мировые эксперты сходятся во мнении, что модель CLI слишком слабая, расплывчатая или откровенно сломанная. См. Джона Скита. и Джо Даффи для двух примеров. - person Douglas; 07.01.2021
comment
Еще одно подтверждение того, что официальная реализация CLR защищает от подобных вещей даже на ARM64: github.com/dotnet/runtime/issues/46911#issuecomment-760004625 - person Kevin Gosse; 14.01.2021

Нет, вам не нужен барьер памяти в конструкторе. Ваше предположение, хоть и демонстрирует некоторую творческую мысль - неверно. Ни один поток не может получить наполовину защищенный экземпляр queue. Новая ссылка "видна" другим потокам только после завершения инициализации. Предположим, что thread_1 - это первый поток, который инициализирует queue - он проходит через код ctor, но ссылка queue в главном стеке по-прежнему равна нулю! только когда существует поток_1, код конструктора назначает ссылку.

См. Комментарии ниже и вопрос, разработанный OP.

person shay__    schedule 10.08.2016
comment
К сожалению, я думаю, вам не хватает тонкостей модели памяти ECMA CLI. Вы можете получить недоделанный экземпляр queue, видимый другим потокам. - person Douglas; 10.08.2016
comment
К сожалению, этот ответ в основном является выдумкой желаемого за действительное. Предполагается, что синхронизация отсутствует. - person ; 10.08.2016
comment
@ Дуглас Должен признаться, я не думал об этом. Тем не менее, вы не найдете никаких доказательств наличия барьеров памяти в System.Collections.Concurrent классах ctors, которые по определению являются потокобезопасными. В вашей теме определение безопасности потоков распространяется на новые регионы. И это круто :) - person shay__; 10.08.2016
comment
System.Collections.Concurrent classes ctors which are by definition thread safe Это неправда. Они небезопасны по определению. Они просто реализации типов, как и любые другие. У них гораздо меньше шансов совершить ошибки, чем в коде, который пишете вы или я, но для них возможно наличие ошибок; их код неверен по определению, но есть высокая степень уверенности в его правильности. - person Servy; 10.08.2016
comment
Всегда приятно напоминать, что ты не знаешь гораздо больше, чем то, чего думаешь, что не знаешь :) - person shay__; 10.08.2016
comment
@Servy: Я предполагаю, что shay__ означало, что классы System.Collections.Concurrent обеспечивают de facto стандарт того, что должно влечь за собой поточно-ориентированное использование. Есть ли формальное определение / более широкий консенсус для потокобезопасности, который охватывает, должна ли она включать инициализацию или нет? - person Douglas; 10.08.2016
comment
@Douglas Thread Safe - вообще довольно бессмысленный термин. Вероятно, лучшее определение, которое вам удастся получить, - это сказать, что программа, использующая несколько потоков, делает то, что должна делать. Вы не можете дать более значимое определение этому термину, и как таковой термин имеет тенденцию быть в значительной степени бесполезным. Если у вас есть объект, к которому вы планируете получить доступ из нескольких потоков, вам необходимо конкретно определить операции, которые вы планируете выполнять, а также приемлемое и неприемлемое поведение этих операций. - person Servy; 10.08.2016
comment
@Servy: Я согласен с тем, что определение дано в общих чертах. Но он регулярно появляется в MSDN: все общедоступные и защищенные члены _ 1_ поточно-ориентированы и могут использоваться одновременно из нескольких потоков. Учитывая, как указывали другие, ConcurrentQueue<T> не включает в себя барьер памяти в конце своего конструктора, тогда либо документация, либо реализация неверны. - person Douglas; 10.08.2016
comment
@Douglas Тот факт, что в документации используется неточно определенный термин, означает, что само утверждение просто бессмысленно. Утверждение, что все открытые и защищенные члены ConcurrentQueue ‹T› являются потокобезопасными, на самом деле ничего не говорит вам о типе. Это не неверно, это просто фактически не делается никаких реальных утверждений, правильных или неправильных. Это как если бы утверждали, что типаж супер крутой. Это утверждение не ошибочно; это утверждение нельзя считать объективно правильным или неправильным. - person Servy; 10.08.2016
comment
@Servy: это может использоваться одновременно из нескольких потоков, что неверно. Конструктор (публичный член) не может использоваться одновременно с другими публичными членами. - person Douglas; 10.08.2016
comment
@Douglas Опять же, вам нужно быть более конкретным, чтобы утверждения имели смысл. Вам нужно будет определить конкретное использование типа вместе с ожидаемым поведением, прежде чем вы сможете сказать, удовлетворяет ли он этим ограничениям. Вы также предполагаете, что проблема возникает только потому, что у вас есть экземпляр объекта конструктора, хотя конструктор еще не завершил работу. - person Servy; 10.08.2016
comment
Пока очередь функционирует должным образом, даже если у вас есть экземпляр, конструктор которого еще не завершен, это не проблема. Если вы не можете наблюдать какую-либо операцию, выполняемую в очереди до завершения построения, что, вероятно, имеет место, учитывая, как эти другие операции реализованы (а именно, использование изменчивых полей), проблем нет. - person Servy; 10.08.2016
comment
@Servy: Любая дальнейшая специфичность противоречила бы цели концепции поточной безопасности. Я понимаю, о чем вы говорите, о повторном использовании и ожидаемом поведении, но повреждение внутреннего состояния никогда не должно быть приемлемым результатом для потоковобезопасных коллекций. Если открытые члены ConcurrentQueue<T> действительно работают правильно даже до завершения конструктора, тогда все в порядке. Но это также налагает ожидание, что мой ThreadSafeQueue<T> также должен использовать volatile или барьеры памяти в своем конструкторе, чтобы соответствовать соглашению .NET о безопасности потоков. - person Douglas; 10.08.2016
comment
@ Дуглас Any further specificity would defeat the purpose of the notion of thread-safety. Это правда. Мне показалось, что я довольно ясно сказал, что это довольно бесполезный термин сам по себе и на самом деле не говорит вам ничего полезного. Речь идет не о соглашениях .NET о безопасности потоков, а о том, что вы хотите делать с этим типом и удовлетворяет ли он это. Не пытайтесь соответствовать полностью неопределенному определению потокобезопасности. Напишите класс, который соответствует контракту, который вам нужен (сейчас похоже, что он этого не делает). - person Servy; 11.08.2016
comment
@Servy: я бы не согласился с тем, что термин потокобезопасность бесполезен, если он по крайней мере гарантирует, что одновременное использование не повредит внутреннее состояние и не вызовет явно неправильное поведение (например, один вызов Enqueue, добавляющий один и тот же элемент дважды ). Да, как вы говорите, в большинстве случаев требуется больше конкретики (например, являются ли перечисления моментальными или могут включать последующие обновления). Но для команды .NET было бы серьезным недостатком, если бы этот термин был настолько неопределенным, как вы подразумеваете. - person Douglas; 11.08.2016
comment
@Douglas Вы только что определили, что потокобезопасность ведет себя правильно. (Это более или менее то, что я определил выше.) Это просто не особенно полезно говорить, если вы не знаете, какое правильное поведение является, или должно быть, или должно быть. Это моральный эквивалент задавания вопросов; использование этого термина не добавляет к беседе ничего полезного, чего бы вы не имели без него. - person Servy; 11.08.2016
comment
Спецификации определяют минимальную модель памяти, которая должна поддерживаться, но это не означает, что более сильная модель памяти не может быть реализована. Возможно, эти System.Collections.Concurrent классы просто предполагают, что модель памяти сильнее указанной, потому что им известны только целевые реализации CLR. Модели памяти также привязаны к ЦП. Например, настольные процессоры вряд ли будут иметь эту проблему ... но, может быть, это уже не так, поскольку я уже давно знаком с архитектурами процессоров. - person Miguel Angelo; 27.02.2017
comment
Также обратите внимание, что барьер памяти не требуется после начала или конца блока кода lock. Никакие чтения или записи не могут быть переупорядочены за пределами границ блока блокировки ... это означает, что если вы обнаружите lock (obj) { }, вы можете предположить, что никакие записи или чтения не будут перемещаться до / после всего блока на другую сторону или изнутри / снаружи на другую сторону. Обратная сторона. - person Miguel Angelo; 27.02.2017