Зачем объявлять переменную как изменчивую и одновременно использовать для нее Interlocked?

Я читаю Параллельное программирование в Windows Джо Даффи. В конце главы «Модели памяти и свобода блокировки» он приводит пример стека без блокировки. Я просмотрел код и одного не понимаю, это необходимость пометки поля m_next как volatile. Потому что там полный барьер памяти с Interlocked.CompareExchange верно? Есть у кого-нибудь идеи?

Я вставил образец кода ниже.

class LockFreeStack<T>
{
    class Node
    {
        internal T m_value;
        internal volatile Node m_next;
    }

    volatile Node m_head;

    void Push(T value)
    {
        Node n = new Node();
        n.m_value = value;

        Node h;
        do
        {
            h = m_head;
            n.m_next = h;
        }
        while (Interlocked.CompareExchange(ref m_head, n, h) != h);
    }

    T Pop()
    {
        Node n;
        do
        {
            n = m_head;
            if (n == null) throw new Exception('stack empty');

        }
        while (Interlocked.CompareExchange(ref m_head, n.m_next, n) != n);

        return n.m_value;
    }
}

person Thanuja Dilhan    schedule 05.06.2021    source источник
comment
albahari.com/threading/part4.aspx#_The_volatile_keyword   -  person Olivier Rogier    schedule 05.06.2021
comment
Я не особо разбираюсь в современной многопоточности и многопоточности .NET, но, возможно, использую Interlocked для нескольких переменных, даже объявленные volatiles, должны гарантировать, что, как сказано в документации, операция является атомарной: Предоставляет атомарные операции для переменных, которые совместно используются несколькими потоками. Это означает, что вы не получите ошибочного результата, если другой поток изменит любую из переменных при выполнении этой операции с использованием Interlocked. Например, если вы заперли все вручную и проделаете эту операцию самостоятельно с помощью нескольких утверждений.   -  person Olivier Rogier    schedule 05.06.2021
comment
Поле m_head также помечено как volatile. Вам тоже нужно объяснение, или это то, что вы понимаете?   -  person Theodor Zoulias    schedule 05.06.2021
comment
@TheodorZoulias Он упомянул, что переменная m_head помечена как изменчивая, чтобы гарантировать, что мы правильно перечитаем ее во время следующей итерации цикла.   -  person Thanuja Dilhan    schedule 05.06.2021


Ответы (1)


Я отдам свои два цента, не будучи на 100% уверенным, что то, что я собираюсь сказать, правильно. Джо Даффи - эксперт мирового уровня в многопоточности, но я думаю, что в этой реализации он был слишком осторожен в отношении перекрестная видимость внутреннего состояния класса LockFreeStack<T>. На мой взгляд, волатильность поля Node.m_next избыточна. Экземпляры Node являются изменяемыми, но они изменяются только перед, когда они связаны во внутреннем связанном списке. После этой фазы они практически неизменны. Эта изменяемая фаза выполняется одним потоком, поэтому нет никаких шансов, что другой поток увидит устаревшую версию экземпляра Node.

Это оставляет только возможность переупорядочения инструкций в качестве причины для объявления Node.m_next как volatile. Поскольку присвоение n.m_next = h; зажато между чтением другого изменчивого поля (m_head) и операцией Interlocked.CompareExchange, я бы предположил, что возможное изменение порядка инструкций, которое может поставить под угрозу правильность реализации, уже предотвращено. , но, как я уже сказал, я не уверен на 100%.

Тем не менее, я почти уверен, что реализация класса LockFreeStack<T> может быть улучшена с точки зрения производительности за счет того, что станет немного более выделяемым, сделав неизменяемым класс Node. В настоящее время (C # 9) этого можно достичь, просто переключившись с типа class на тип _ 13_. Вот как могла бы выглядеть такая реализация:

class LockFreeStack<T>
{
    record Node(T Value, Node Next) { }

    Node _head;

    void Push(T value)
    {
        Node h = Volatile.Read(ref _head);
        while (true)
        {
            Node n = new Node(value, h);
            var original = Interlocked.CompareExchange(ref _head, n, h);
            if (original == h) break;
            h = original;
        }
    }

    T Pop()
    {
        Node h = Volatile.Read(ref _head);
        while (true)
        {
            if (h == null) throw new Exception("Stack empty");
            var original = Interlocked.CompareExchange(ref _head, h.Next, h);
            if (original == h) break;
            h = original;
        }
        return h.Value;
    }
}

Обратите внимание, что стоимость волатильности возникает только один раз для каждой Push или Pop операции. Можно даже утверждать, что Volatile.Read из поле _head можно было бы полностью опустить, поскольку возможное устаревшее значение _head в любом случае будет исправлено первым _ 20_, затратив лишь дополнительную итерацию while (true) цикла.

person Theodor Zoulias    schedule 05.06.2021