При внедрении класса, предназначенного для обеспечения потокобезопасности, следует ли включить барьер памяти в конец его конструктора, чтобы обеспечить завершение инициализации любых внутренних структур до их доступа? Или пользователь должен вставить барьер памяти, прежде чем сделать экземпляр доступным для других потоков?Должен ли класс, защищенный потоками, иметь защитный барьер памяти в конце его конструктора?
упрощенный вопрос:
Есть ли раса опасность в коде ниже, может дать ошибочное поведение из-за отсутствие барьера памяти между инициализацией и доступом к поточно-классу? Или должен ли сам защищаемый потоком класс защищаться от этого?
ConcurrentQueue<int> queue = null;
Parallel.Invoke(
() => queue = new ConcurrentQueue<int>(),
() => queue?.Enqueue(5));
Обратите внимание, что это приемлемо для программы не епдиеее ничего, как это произойдет, если второй делегат выполняет перед первым. (Оператор с нулевым условием ?.
здесь защищен от .) Однако для программы не должно быть приемлемым многократное перетащить IndexOutOfRangeException
, NullReferenceException
, enqueue 5
, застревать в бесконечном цикле или сделать любой другой странный вещи, вызванные расовой опасностью на внутренних структурах.
Разработал вопрос:
В частности, представьте себе, что я реализовывал простую поточно-обертку для очереди. (Я знаю, что .NET уже предоставляет ConcurrentQueue<T>
, это всего лишь пример.) Я мог бы написать:
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 для классов, помеченных как потокобезопасное?
Редактировать: Это расширенная тема для резьбы, поэтому я понимаю путаницу в некоторых комментариях. Экземпляр может отображаться в виде полупека при доступе из других потоков без надлежащей синхронизации. Этот вопрос широко обсуждается в контексте двойной проверки блокировки, которая нарушена в соответствии с спецификацией CLI ECMA без использования барьеров памяти (например, через volatile
). Per Jon Skeet:
модели памяти Java не гарантирует, что конструктор завершается до ссылка на новый объект присваивается экземпляр. Модель памяти Java претерпела переработку для версии 1.5, но после этого блокировка двойной проверки по-прежнему прерывается без изменчивой переменной (, как в C#).
Без каких-либо барьеров памяти он также поврежден в спецификации ECMA CLI. Возможно, что в модели памяти .NET 2.0 (которая сильнее спецификации ECMA) это безопасно, но я бы предпочел не полагаться на эти более сильные семантики, особенно если есть какие-либо сомнения относительно безопасности.
Исходный код 'ConcurrentQueue', о котором вы упомянули, не имеет никакой защиты в его конструкторе. Сделайте из этого что хочешь. http://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentQueue.cs,18bcbcbdddbcfdcb –
Как насчет инициализации потребителя с использованием Lazy, который делает инициализацию потокобезопасной? :) –
user3185569
Запрет конструктора, который на самом деле имеет асинхронные вызовы внутри него, может ли ссылка даже быть настроена для ссылки на экземпляр до создания экземпляра? – Uueerdo