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

Является ли барьер памяти или атомная операция в цикле "занятый-ждать"?

Рассмотрим следующую реализацию spin_lock(), первоначально из этого answer:

void spin_lock(volatile bool* lock)  {  
    for (;;) {
        // inserts an acquire memory barrier and a compiler barrier
        if (!__atomic_test_and_set(lock, __ATOMIC_ACQUIRE))
            return;

        while (*lock)  // no barriers; is it OK?
            cpu_relax();
    }
}

Что я уже знаю:

  • volatile не позволяет компилятору оптимизировать out *lock перечитывать на каждой итерации цикла while;
  • volatile не вставляет ни памяти, ни ограничения компилятора;
  • такая реализация действительно работает в GCC для x86 (например, в ядре Linux) и некоторых других архитектурах;
  • по крайней мере один барьер памяти и компилятора требуется в реализации spin_lock() для общей архитектуры; этот пример вставляет их в __atomic_test_and_set().

Вопросы:

  • Здесь volatile достаточно или существуют какие-либо архитектуры или компиляторы, где в цикле while требуется защита памяти или компилятора или атомная операция?

    1.1 Согласно C++ стандартам?

    1.2 На практике для известных архитектур и компиляторов, особенно для GCC и платформ, он поддерживает?

  • Является ли эта реализация безопасной для всех архитектур, поддерживаемых GCC и Linux? (По крайней мере, это неэффективно на некоторых архитектурах, верно?)
  • Безопасен ли цикл while в соответствии с C++11 и его моделью памяти?

Есть несколько связанных вопросов, но я не смог построить явный и однозначный ответ от них:

4b9b3361

Ответ 1

  • Здесь достаточно летучих или существуют какие-либо архитектуры или компиляторы, где в цикле while требуется память или компилятор барьера или атома?

изменяет код изменчивого кода. Да, но не обязательно так быстро, как если бы был барьер памяти. В какой-то момент произойдет некоторая форма синхронизации, и новое состояние будет считано из переменной, но нет никаких гарантий того, что произошло в другом месте кода.

1.1 В соответствии со стандартами С++?

От cppreference: memory_order

Это модель памяти и порядок памяти, который определяет обобщенное оборудование, над которым должен работать код. Чтобы сообщение проходило между потоками выполнения, должно происходить взаимодействие между потоками и событиями. Это требует либо...

  • A синхронизируется с B
  • A имеет операцию std:: atomic перед тем, как B
  • Косвенно синхронизируется с B (через X).
  • A секвенирован до X, который происходит между потоками до B
  • Промежуточный переход происходит до того, как X и X будут выполняться до B.

Поскольку вы не выполняете ни один из этих случаев, будут формы вашей программы, где на каком-то текущем аппаратном обеспечении он может выйти из строя.

На практике конец временного фрагмента приведет к тому, что память станет когерентной, или любая форма барьера на цепочке без спин-блокировки обеспечит очистку кэшей.

Не уверен в причинах неустойчивого чтения, получая "текущее значение".

1.2 На практике для известных архитектур и компиляторов, особенно для GCC и платформ, он поддерживает?

Поскольку код не согласуется с обобщенным ЦП, от C++11, то, скорее всего, этот код не сможет выполнить с версиями С++, которые пытаются придерживаться стандарта.

Из cppreference: const volatile qualifiers Неустойчивый доступ останавливает оптимизацию от перемещения работы от нее до нее и от нее до нее.

"Это делает изменчивые объекты подходящими для связи с обработчиком сигнала, но не с другим потоком выполнения"

Таким образом, реализация должна гарантировать, что инструкции считываются из памяти, а не из любой локальной копии. Но он не должен обеспечивать, чтобы волатильная запись была пропущена через кеши, чтобы обеспечить согласованное представление по всем процессорам. В этом смысле нет временной границы того, как долго после того, как запись в изменчивую переменную станет видимой для другого потока.

Также см. kernel.org, почему волатильность почти всегда неправильна в ядре

Является ли эта реализация безопасной для всех архитектур, поддерживаемых GCC и Linux? (Это по крайней мере неэффективно на некоторых архитектурах, верно?)

Нет гарантии, что измененное сообщение выходит из потока, который устанавливает его. Так что не очень безопасно. В Linux это может быть безопасно.

Безопасен ли цикл while в соответствии с С++ 11 и его моделью памяти?

Нет - поскольку он не создает никаких примитивов обмена сообщениями между потоками.

Ответ 2

Это важно: в С++ volatile нет ничего вообще с concurrency! Цель volatile - сообщить компилятору, что он не должен оптимизировать обращения к затронутому объекту. Он не сообщает CPU о чем-либо, в первую очередь потому, что CPU уже будет знать, будет ли память volatile или нет. Целью volatile является эффективное обращение к I/O с отображением памяти.

В разделе 1.10 [intro.multithread] очень понятен стандарт С++, что несинхронизированный доступ к объекту, который модифицирован в одном потоке и к которому осуществляется доступ (измененный или прочитанный) в другом потоке, - это поведение undefined. Примитивы синхронизации, избегающие поведения undefined, являются библиотечными компонентами, такими как атомные классы или мьютексы. В этом разделе упоминается volatile только в контексте сигналов (т.е. Как volatile sigatomic_t) и в контексте продвижения вперед (т.е. Что поток в конечном итоге сделает что-то, что имеет наблюдаемый эффект, например, доступ к объекту volatile или делая ввод-вывод). В связи с синхронизацией не упоминается volatile.

Таким образом, несинхронизированная оценка переменной, разделяемой по потокам, приводит к поведению undefined. Объявлено ли оно volatile или нет, не имеет значения для этого поведения undefined.

Ответ 3

Из страницы Википедии о барьерах памяти:

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

Для меня это означает, что Itanium требует подходящего заграждения, чтобы сделать чтение/запись видимым для других процессоров, но на самом деле это может быть просто для целей заказа. Вопрос, я думаю, действительно сводится к следующему:

Существует ли архитектура, в которой процессор никогда не сможет обновить свой локальный кеш, если не будет проинструктирован об этом? Я не знаю ответа, но если вы задали вопрос в этой форме, то кто-то иначе может. В такой архитектуре ваш код потенциально переходит в бесконечный цикл, где чтение *lock всегда видит одно и то же значение.

В терминах общей законности С++ один атомный тест и набор в вашем примере недостаточен, поскольку он реализует только один забор, который позволит вам видеть начальное состояние *lock при входе в цикл while но не видеть, когда он изменяется (что приводит к поведению undefined, так как вы читаете переменную, которая изменяется в другом потоке без синхронизации) - поэтому ответ на ваш вопрос (1.1/3) - нет.

С другой стороны, на практике ответ на (1.2/2) да (учитывая GCC volatile semantics), пока архитектура гарантирует согласованность кэша без явных зазоров памяти, что справедливо для x86 и, вероятно, для многих архитектур, но я не могу дать однозначного ответа на вопрос, верно ли это для всех архитектур, поддерживаемых GCC. Однако, как правило, неразумно сознательно полагаться на конкретное поведение кода, технически undefined поведение в соответствии с спецификацией языка, особенно если можно получить тот же результат без этого.

Кстати, учитывая, что memory_order_relaxed существует, кажется мало причин не использовать его в этом случае, а не пытаться оптимизировать вручную, используя неатомные чтения, т.е. изменить цикл while в вашем примере на:

    while (atomic_load_explicit(lock, memory_order_relaxed)) {
        cpu_relax();
    }

В x86_64, например, атомная нагрузка становится регулярной инструкцией mov, а оптимизированный вывод сборки по существу такой же, как и для вашего первоначального примера.