Заборы в С++ 0x, гарантирует только атомы или память в целом

С++ 0x draft имеет представление о заборе, которое кажется очень отличным от уровня заграждений уровня CPU/чипа, или сказать, что ожидают ядро ​​Linux-ядра из заборов. Вопрос заключается в том, действительно ли проект подразумевает чрезвычайно ограниченную модель, или формулировка просто плохая, и на самом деле она подразумевает истинные заборы.

Например, в разделе 29.8 "Заборы" указываются такие вещи, как:

Заблокировочный затвор A синхронизируется с приобретать забор B, если существуют атомные операции X и Y, работающие на некоторый атомный объект M, такой, что A является последовательность до X, X модификаций M, Y - секвенированы до B, а Y - значение, написанное X, или значение, записанное любой стороной в гипотетическом освобождение последовательности X будет голова, если она были операцией выпуска.

Он использует эти термины atomic operations и atomic object. В проекте есть такие атомные операции и методы, но означает ли это только те, что? Заборный забор звучит как забор магазина. Забор магазина, который не гарантирует запись всех данных перед забором, почти бесполезен. Аналогично для нагрузки (приобретать) забор и полный забор.

Итак, являются ли заграждения/барри в правильных заборах С++ 0x, а формулировка просто невероятно плоха или они чрезвычайно ограничены/бесполезны, как описано?


В терминах С++, скажем, у меня есть этот существующий код (предполагая, что забор доступен как конструкции высокого уровня прямо сейчас - вместо того, чтобы использовать __sync_synchronize в GCC):

Thread A:
b = 9;
store_fence();
a = 5;

Thread B:
if( a == 5 )
{
  load_fence();
  c = b;
}

Предположим, что a, b, c имеют размер, имеющий атомную копию на платформе. Вышеизложенное означает, что c будет присваиваться только 9. Обратите внимание, что нам не важно, когда Thread B видит a==5, только когда он делает это, также видит b==9.

Что такое код в С++ 0x, который гарантирует одинаковые отношения?


ANSWER. Если вы прочтете мой выбранный ответ и все комментарии, вы получите суть ситуации. С++ 0x, похоже, заставляет вас использовать атом с ограждениями, тогда как обычный аппаратный забор не имеет этого требования. Во многих случаях это все равно можно использовать для замены одновременных алгоритмов, пока sizeof(atomic<T>) == sizeof(T) и atomic<T>.is_lock_free() == true.

Однако, к сожалению, is_lock_free не является constexpr. Это позволило бы использовать его в static_assert. Наличие atomic<T> вырожденного использования блокировок, как правило, является плохой идеей: атомные алгоритмы, которые используют мьютексы, будут иметь ужасные проблемы конфликтов по сравнению с алгоритмом, разработанным мьютексом.

Ответ 1

Заборы обеспечивают упорядочение по всем данным. Однако, чтобы гарантировать, что операция забора из одного потока будет видна на секунду, вам нужно использовать атомные операции для флага, иначе у вас будет гонка данных.

std::atomic<bool> ready(false);
int data=0;

void thread_1()
{
    data=42;
    std::atomic_thread_fence(std::memory_order_release);
    ready.store(true,std::memory_order_relaxed);
}

void thread_2()
{
    if(ready.load(std::memory_order_relaxed))
    {
        std::atomic_thread_fence(std::memory_order_acquire);
        std::cout<<"data="<<data<<std::endl;
    }
}

Если thread_2 читает ready как true, то ограждения гарантируют, что data можно безопасно прочитать, а выход будет data=42. Если ready читается как false, то вы не можете гарантировать, что thread_1 выдал соответствующий забор, поэтому забор в потоке 2 все равно не предоставит необходимые гарантии для заказа --- если if в thread_2 был опущен, доступ к data был бы расчетом данных и undefined, даже с забором.

Уточнение: A std::atomic_thread_fence(std::memory_order_release) обычно эквивалентен ограждению магазина и, скорее всего, будет реализован как таковой. Однако одиночный забор на одном процессоре не гарантирует упорядочения памяти: вам нужен соответствующий забор на втором процессоре, И, вам нужно знать, что при запуске заборного ограждения эффекты заборного ограждения были видны этому второму процессору. Очевидно, что если CPU A выдает сборку забора, а затем через 5 секунд процессор B выдает забор, то этот забор не может синхронизироваться с заборным ограждением. Если у вас нет средств проверки того, был ли забор выпущен на другом CPU, код на CPU A не может определить, выдает ли его забор до или после ограждения CPU B.

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

Конечно, можно использовать более сильный механизм, такой как мьютекс, но это сделает отдельный забор бессмысленным, поскольку мьютекс обеспечит забор.

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

Код, написанный для использования ограждений, зависящих от процессора, может быть легко изменен для использования ограждений С++ 0x, если операции, используемые для проверки синхронизации (а не те, которые используются для доступа к синхронизированным данным), являются атомарными. Существующий код вполне может опираться на атомарность простых нагрузок и хранилищ на заданном CPU, но для преобразования в С++ 0x потребуются атомные операции для этих проверок, чтобы обеспечить гарантии порядка.

Ответ 2

Я понимаю, что они правильные заборы. Косвенным доказательством является то, что в конце концов они предназначены для сопоставления с функциями, найденными в реальном аппаратном обеспечении и которые позволяют эффективно выполнять алгоритмы синхронизации. Как вы говорите, ограждения, которые применяются только к некоторым конкретным значениям, являются 1. бесполезными и 2. не найдены на текущем оборудовании.

Как сказано, AFAICS в разделе, который вы цитируете, описывает взаимосвязь между синхронизациями и элементами атома. Для определения того, что это означает, см. Раздел 1.10 Многопоточные исполнения и расписания данных. Опять же, AFAICS, это не означает, что ограждения применимы только к атомным объектам, но я подозреваю, что смысл в том, что, хотя обычные нагрузки и хранилища могут проходить захват и освобождение ограждений обычным способом (только одно направление), атомные нагрузки/магазины не могут.

Wrt. я понимаю, что на всех объектах, поддерживаемых Linux, правильно выровнены простые целочисленные переменные, чьи sizeof() <= sizeof (* void) являются атомарными, поэтому Linux использует обычные целые числа в качестве переменных синхронизации (то есть ядерные операции ядра Linux работают с нормальными целыми переменными). С++ не хочет налагать такое ограничение, следовательно, отдельные атомные целые типы. Кроме того, в С++ операции с целыми целыми атомами подразумевают барьеры, тогда как в ядре Linux все барьеры явны (что является очевидным, поскольку без поддержки компилятора для атомных типов это то, что нужно делать).