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

В проекте C++0x есть понятие "заборы", которое кажется очень отличным от понятия "заборы" на уровне процессора / чипа, или сказать, что ребята из ядра Linux ожидают от заборов. Вопрос в том, действительно ли проект подразумевает крайне ограниченную модель, или формулировка просто плохая, и на самом деле это подразумевает истинные заборы.

Например, под 29,8 Заборы это заявляет вещи как:

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

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

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


С точки зрения C++, скажем, у меня есть этот существующий код (при условии, что заборы доступны как конструкции высокого уровня прямо сейчас - вместо того, чтобы использовать __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, Обратите внимание, нам все равно, когда видит поток B a==5просто, что когда он это делает, он также видит b==9,

Какой код в C++0x гарантирует такие же отношения?


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

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

2 ответа

Решение

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

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 будет гонка данных и неопределенное поведение, даже с забором.

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

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

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

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

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

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

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

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

Другие вопросы по тегам