2016-08-17 3 views
6

Предположит, что я проектировал поточно-класс, который оборачивает внутреннюю коллекцию:Должны ли волатильные и readonly быть взаимоисключающими?

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

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

    // ... 
} 

Основываясь на my other question, выше реализация глючит, так как раса опасность может возникнуть при его инициализации выполняется одновременно с его использованием:

ThreadSafeQueue<int> tsqueue = null; 

Parallel.Invoke(
    () => tsqueue = new ThreadSafeQueue<int>(), 
    () => tsqueue?.Enqueue(5)); 

Код, приведенный выше, является приемлемо недетерминированным: элемент может быть или не быть выставлен в очередь. Однако в рамках текущей реализации он также нарушается и может привести к непредсказуемому поведению, например, бросать IndexOutOfRangeException, NullReferenceException, выставлять один и тот же элемент несколько раз или застревать в бесконечном цикле. Это происходит, так как Enqueue вызов может запустить после новый экземпляр был назначен локальной переменной tsqueue, но перед тем инициализацию внутреннего _queue поля завершается (или, как представляется, полный).

Per Jon Skeet:

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

Эта раса опасность может быть решен путем добавления барьера памяти в конструктор:

public ThreadSafeQueue() 
    { 
     Thread.MemoryBarrier(); 
    } 

Эквивалентен, это может быть более сжато решено путем создания поля летучего:

private volatile readonly Queue<T> _queue = new Queue<T>(); 

Однако , последний запрещен компилятором C#:

'Program.ThreadSafeQueue<T>._queue': a field cannot be both volatile and readonly 

Учитывая, что вышесказанное кажется оправданным прецедентом для volatile readonly, является ли это недостатком в оформлении языка?

Я знаю, что можно просто удалить readonly, так как это не влияет на открытый интерфейс класса. Тем не менее, это не относится к делу, так как это можно сказать и для readonly в целом. Я также знаю существующий вопрос «Why readonly and volatile modifiers are mutually exclusive?»; однако, они занялись другой проблемой.

Конкретный сценарий: Эта проблема, похоже, влияет на код в пространстве имен System.Collections.Concurrent самой библиотеки классов .NET Framework. Вложенный класс ConcurrentQueue<T>.Segment имеет несколько полей, которые только когда-либо назначаются внутри конструктора: m_array, m_state, m_index и m_source. Из них только m_index объявляется как только для чтения; другие не могут быть - хотя и должны - поскольку они должны быть объявлены как летучие для удовлетворения требований безопасности потоков.

private class Segment 
{ 
    internal volatile T[] m_array;     // should be readonly too 
    internal volatile VolatileBool[] m_state;  // should be readonly too 
    private volatile Segment m_next; 
    internal readonly long m_index; 
    private volatile int m_low; 
    private volatile int m_high; 
    private volatile ConcurrentQueue<T> m_source; // should be readonly too 

    internal Segment(long index, ConcurrentQueue<T> source) 
    { 
     m_array = new T[SEGMENT_SIZE];    // field only assigned here 
     m_state = new VolatileBool[SEGMENT_SIZE]; // field only assigned here 
     m_high = -1; 
     m_index = index;       // field only assigned here 
     m_source = source;       // field only assigned here 
    } 

    internal void Grow() 
    { 
     // m_index and m_source need to be volatile since race hazards 
     // may otherwise arise if this method is called before 
     // initialization completes (or appears to complete) 
     Segment newSegment = new Segment(m_index + 1, m_source); 
     m_next = newSegment; 
     m_source.m_tail = m_next; 
    } 

    // ... 
} 
+1

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

+1

Неразумно думать, что Эрик Липперт может перезвонить. Если он все еще помнит что-нибудь о C#;) – adv12

+0

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

ответ

4

readonly поля полностью доступны для записи из тела конструктора. Действительно, можно использовать volatile для доступа к полю readonly, чтобы вызвать барьер памяти. Ваш случай, я думаю, хороший пример для этого (и это предотвращает язык).

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

Вы можете использовать следующий метод обхода:

class Program 
{ 
    readonly int x; 

    public Program() 
    { 
     Volatile.Write(ref x, 1); 
    } 
} 

Я проверил, что это компилируется. Я не был уверен, разрешено ли создавать ref в поле readonly или нет, но это так.

Почему язык предотвращает readonly volatile? Лучше всего предположить, что это должно помешать вам совершить ошибку. Большую часть времени это было бы ошибкой. Это похоже на использование await внутри lock: Иногда это совершенно безопасно, но большую часть времени это не так.

Возможно, это должно было быть предупреждением.

Volatile.Write не существовало во время C# 1.0, поэтому случай для этого предупреждения для 1.0 сильнее. Теперь, когда есть обходной путь, дело в том, что ошибка является сильной.

Я не знаю, разрешает ли CLR readonly volatile. Если да, это может быть другой причиной. CLR имеет стиль, позволяющий выполнять большинство операций, которые разумно реализовать. C# гораздо более ограничительный, чем CLR. Поэтому я уверен (без проверки), что CLR позволяет это.

+1

Это хороший момент (и аналогия). Если риск ошибок, возникающих из-за возможности «readonly volatile», больше, чем для тех, которые возникают из-за принудительного пропуска «readonly», тогда дизайн языка чувствует себя оправданным. Хотя в идеале ошибка должна в идеале указывать на то, что некоторые методы разумны и предложили альтернативу «Volatile.Write». – Douglas

0

Прежде всего, это, кажется, ограничение накладывается на язык, а не платформа:

.field private initonly class SomeTypeDescription modreq ([mscorlib]System.Runtime.CompilerServices.IsVolatile) SomeFieldName  

компилируется нормально, и я не мог найти любую цитату о том, что initonly (только для чтения) не может быть в сочетании с modreq ([mscorlib]System.Runtime.CompilerServices.IsVolatile) (volatile).

Насколько я понимаю, описанная ситуация, вероятно, происходит из-за переопределения команд низкого уровня. Код, который создает объект и помещает его в поле выглядит следующим образом:

newobj  instance void SomeClassDescription::.ctor() 
stfld  SomeFieldDescription 

И как ECMA гласит:

Инструкция newobj выделяет новый экземпляр класса, связанный с CTOR и инициализирует все поля в новом экземпляре равны 0 (соответствующего типа) или null, если это необходимо. Затем он вызывает конструктор с заданными аргументами вместе с вновь созданным экземпляром. После того, как конструктор был вызван, теперь инициализированная ссылка на объект помещается в стек.

Итак, насколько я понимаю, пока инструкции не не поменялись местами (что имхо возможно потому, что возвращение в адрес созданного объекта и заполнения этого объекта являются магазины в разных местах), вы всегда видите либо полностью инициализирован объект или null при чтении из другого потока. Это может быть гарантировано с помощью изменчивости. Это предотвратило бы замену:

newobj 
volatile. 
stfld 

P.s. Это не ответ сам по себе. Я не знаю, почему C# запрещает readonly volatile.

1
ThreadSafeQueue<int> tsqueue = null; 

Parallel.Invoke(
    () => tsqueue = new ThreadSafeQueue<int>(), 
    () => tsqueue?.Enqueue(5)); 

В вашем примере проблема в том, что tsqueue опубликована в не-нить безопасным способом. И в этом случае абсолютно возможно получить частично построенный объект на таких архитектурах, как ARM. Итак, отметьте tsqueue как volatile или присвойте значение Volatile.Write.

Эта проблема, кажется, затрагивает код в пространстве имен .NET Framework Class Библиотеки System.Collections.Concurrent . Вложенный класс ConcurrentQueue.Segment имеет несколько полей, которые являются , которые когда-либо были назначены внутри конструктора: m_array, m_state, m_index, и m_source. Из них только m_index объявляется как readonly; другие не могут быть - хотя они должны - так как они должны быть , заявленные как летучие для удовлетворения требований безопасности потока.

маркировка поля как readonly просто добавляют некоторые ограничения, которые компилятор проверяет, и что JIT могут впоследствии использовать для оптимизации (но JIT достаточно умен, чтобы понять, что поле является readonly даже без этого ключевого слова в некоторых случаях). Но маркировка этих конкретных полей volatile гораздо важнее из-за параллелизма. private и internal поля находятся под контролем автора этой библиотеки, поэтому абсолютно нормально опускать readonly.

Смежные вопросы