2016-08-10 2 views
14

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

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

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

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) это безопасно, но я бы предпочел не полагаться на эти более сильные семантики, особенно если есть какие-либо сомнения относительно безопасности.

+1

Исходный код 'ConcurrentQueue ', о котором вы упомянули, не имеет никакой защиты в его конструкторе. Сделайте из этого что хочешь. http://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentQueue.cs,18bcbcbdddbcfdcb –

+0

Как насчет инициализации потребителя с использованием Lazy , который делает инициализацию потокобезопасной? :) – user3185569

+1

Запрет конструктора, который на самом деле имеет асинхронные вызовы внутри него, может ли ссылка даже быть настроена для ссылки на экземпляр до создания экземпляра? – Uueerdo

ответ

1

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); 
    } 
}); 
+1

+1: Это то, что я сейчас делаю, учитывая мою неопределенность. Но я хотел бы знать, следует ли ожидать, что код будет работать * без * синхронизации потоков с потребителем. Другими словами: если бы я был тем, кто внедрял потокобезопасный класс, должен ли я иметь возможность вызывать мой класс «поточно-безопасным», если потребителям все еще нужно использовать «Lazy »? – Douglas

+0

@ Дуглас проверить отредактированный ответ. – user3185569

+1

Я не понимаю редактирование. Мой исходный код никогда бы не выполнил инициализацию дважды. – Douglas

0

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

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

+1

К сожалению, я думаю, что вам не хватает тонкостей модели памяти CLI ECMA. Вы * можете * получить полупродутый экземпляр «очереди», видимый для других потоков. – Douglas

+1

Этот ответ, к сожалению, в основном принимает желаемое за действительное. Он принимает синхронизацию, когда ее нет. – hvd

+1

@ Дуглас Я должен признать, что я не думал об этом. Тем не менее, вы не обнаружите никаких признаков барьеров памяти в «System.Collections».Concurrent' классы ctors, которые по определению ** потокобезопасны. Ваша тема расширяет определение безопасности потоков для новых регионов. И это довольно круто :) –

0

В ответ на ваш вопрос: упрощенный

ConcurrentQueue<int> queue = null; 

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

Это, безусловно, возможно, что ваш код может попытаться вызвать queue.Enqueue(5)queue перед тем, имеет значение, но это не все, что вы могли бы защитить от из конструктора от Queue. queue на самом деле не будет назначена ссылка на новый экземпляр до завершения конструктора.

+0

Если второй делегат выполняется до того, как назначен 'queue', он просто ничего не сделает; нулевой условный оператор '? .' предотвращает' NullReferenceException'. Тем не менее, я спрашиваю, что произойдет, если второй делегат выполнит после того, как 'queue' был назначен, но до того, как его конструктор завершил выполнение с точки зрения другого потока. – Douglas

+0

Ах, да, я пропустил там '?'. Я понимаю, что этого не может быть. Ссылка не назначается до тех пор, пока _after_ конструктор не будет завершен. –

+1

'queue на самом деле не будет назначать ссылку на новый экземпляр до тех пор, пока конструктор не завершит выполнение.' Это справедливо для одной программы с резьбой. В многопоточном контексте вы можете наблюдать, как эти операции происходят не по порядку. – Servy

1

Unrelated, но все-таки интересно, что в Java для всех конечных полей, которые написаны внутри конструктора было бы два забора, написанные после того, как конструктор существует: StoreStore и LoadStore - что бы сделать публикацию ссылки потокобезопасной.

+0

Thanks; хорошо знать! – Douglas