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

Рассмотрим следующее spin_lock() реализация, родом из этот ответ:

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

Вопросы:

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

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

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

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

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

21

Решение

  1. Является ли здесь достаточно изменчивым или есть какие-либо архитектуры или компиляторы, где в цикле while требуется память, барьер компилятора или атомарная операция?

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

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

От cppreference: memory_order

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

  • А синхронизируется с Б
  • A имеет стандартную атомарную операцию перед B
  • Косвенно синхронизируется с B (через X).
  • A секвенируется перед X, который между потоками происходит до B
  • Interthread происходит до X и X interthread происходит до B.

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

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

Не уверен в причинах чтения волатильности, получая «текущее значение».

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

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

От cppreference: const volatile квалификаторы
Волатильный доступ не позволяет оптимизаторам перемещать работу с «до» и «после» и с «после» до нее.

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

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

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

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

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

Является ли цикл while безопасным в соответствии с C ++ 11 и его моделью памяти?

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

1

Другие решения

Это важно: в C ++ volatile имеет ничего такого вообще делать с параллелизмом! Цель volatile это рассказать компилятор что он не должен оптимизировать доступ к затронутому объекту. Оно делает не сказать ЦПУ что-нибудь, в первую очередь потому, что ЦП уже знает, будет ли память volatile или нет. Цель volatile эффективно бороться с отображением памяти ввода-вывода.

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

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

13

От Страница Википедии о барьерах памяти:

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

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

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

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

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

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

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

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

4
По вопросам рекламы [email protected]