Подтвердить что ты не робот

Если в потокобезопасном классе есть барьер памяти в конце его конструктора?

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

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

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

ConcurrentQueue<int> queue = null;

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

Обратите внимание: допустимо, чтобы программа ничего не выдавала в очередь, как это было бы, если второй делегат выполняет перед первым. (Оператор с нулевым условием ?. защищает от NullReferenceException здесь.) Однако для программы не должно быть допустимо несколько раз бросать 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, но после этого блокировка с двойной проверкой все еще прерывается без изменчивой переменной (, как в С#).

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

4b9b3361

Ответ 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);
    }
});

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

ConcurrentQueue<int> queue = null;

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

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