Понимание атомарных переменных и операций

Я читаю о атомарных типах boost и std (c ++ 11) и операциях снова и снова, и все же я не уверен, что понимаю это правильно (а в некоторых случаях я вообще не понимаю). Итак, у меня есть несколько вопросов по этому поводу.

Мои источники я использую для обучения:


Рассмотрим следующий фрагмент:

atomic<bool> x,y;

void write_x_then_y()
{
x.store(true, memory_order_relaxed);
y.store(true, memory_order_release);
}

# 1: Это эквивалентно следующему?

atomic<bool> x,y;

void write_x_then_y()
{
x.store(true, memory_order_relaxed);
atomic_thread_fence(memory_order_release);    // *1
y.store(true, memory_order_relaxed);          // *2
}

# 2: верно ли следующее утверждение?

Строка * 1 гарантирует, что когда операции, выполненные в этой строке (например, * 2), видны (для другого потока, использующего acqu), код выше * 1 также будет виден (с новыми значениями).


Следующий отрывок расширяет вышеперечисленные:

void read_y_then_x()
{
if(y.load(memory_order_acquire))
{
assert(x.load(memory_order_relaxed));
}
}

# 3: это эквивалентно следующему?

void read_y_then_x()
{
atomic_thread_fence(memory_order_acquire);    // *3
if(y.load(memory_order_relaxed))              // *4
{
assert(x.load(memory_order_relaxed));     // *5
}
}

# 4: Верны ли следующие утверждения?

  • Строка * 3 гарантирует, что если будут видны некоторые операции в порядке деблокирования (в другом потоке, например * 2), то будут видны также все операции выше порядка деблокирования (например, * 1).
  • Это означает, что assert в * 5 никогда не потерпит неудачу (со значением false по умолчанию).
  • Но это не гарантирует, что даже если физически (в процессоре) * 2 произойдет раньше, чем * 3, это будет видно по фрагменту кода выше (работающему в другом потоке) — функция read_y_then_x () все еще может читать старые значения. Единственное, что гарантировано, так это то, что если y истинно, то x также будет истинно.

# 5: Приращение (операция добавления 1) к атомному целому числу может быть memory_order_relaxed, и никакие данные не будут потеряны. Единственная проблема — порядок и время видимости результата.


По словам буста, работает следующий счетчик ссылок:

#include <boost/intrusive_ptr.hpp>
#include <boost/atomic.hpp>

class X {
public:
typedef boost::intrusive_ptr<X> pointer;
X() : refcount_(0) {}

private:
mutable boost::atomic<int> refcount_;
friend void intrusive_ptr_add_ref(const X * x)
{
x->refcount_.fetch_add(1, boost::memory_order_relaxed);
}
friend void intrusive_ptr_release(const X * x)
{
if (x->refcount_.fetch_sub(1, boost::memory_order_release) == 1) {
boost::atomic_thread_fence(boost::memory_order_acquire);
delete x;
}
}
};

# 6 Почему для уменьшения используется memory_order_release? Как это работает (в контексте)? Если то, что я написал ранее, является правдой, что делает возвращаемое значение самым последним, особенно когда мы используем метод получения ПОСЛЕ чтения, а не до / во время?

# 7 Почему возникает заказ на приобретение после того, как счетчик ссылок достигнет нуля? Мы только что прочитали, что счетчик равен нулю и никакая другая атомарная переменная не используется (сам указатель не помечен / не используется как таковой).

0

Решение

1: Нет. Разделительный забор синхронизируется со всеми операциями захвата и ограждениями. Если бы был третий atomic<bool> z которым манипулировали в третьем потоке, ограждение также синхронизировалось бы с этим третьим потоком, что не является необходимым. При этом они будут действовать одинаково на x86, но это потому, что x86 имеет очень сильную синхронизацию. Архитектуры, используемые в 1000 основных системах, как правило, слабее.

2: Да, это правильно. Забор гарантирует, что если вы видите все, что следует, вы также видите все, что предшествовало.

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

4: Все это правда. Атомные операции гарантируют последовательность. Они не всегда гарантируют, что все происходит в том порядке, который вы хотели, они просто предотвращают патологические порядки, которые разрушают ваш алгоритм.

5: правильно. Расслабленные операции действительно атомарны. Они просто не синхронизируют дополнительную память

6: для любого данного атомного объекта MC ++ гарантирует, что существует «официальный» порядок операций на M, Вы не видите «последнее» значение для M столько, сколько C ++ и процессор гарантируют, что все потоки будут видеть последовательный ряд значений для M, Если два потока увеличивают refcount, а затем уменьшают его, нет гарантии, что один из них уменьшит его до 0, но есть гарантия, что один из них увидит, что он уменьшил его до 0. У них обоих нет способа видим, что они уменьшили 2-> 1 и 2-> 1, но каким-то образом refcount объединил их в 0. Один поток всегда будет видеть 2-> 1, а другой — 1-> 0.

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

7: Этот хитрее. Короткая версия для 7 состоит в том, что декрементом является порядок выпуска, потому что некоторый поток должен будет запустить деструктор для x, и мы хотим убедиться, что он видит все операции над x, выполненные во всех потоках. Использование порядка выпуска в деструкторе удовлетворяет эту потребность, потому что вы можете доказать, что он работает. Тот, кто отвечает за удаление x, получает все изменения, прежде чем сделать это (используя забор, чтобы убедиться, что атомы в удалителе не перемещаются вверх). Во всех случаях, когда потоки выпускают свои собственные ссылки, очевидно, что все потоки будут иметь декремент порядка выпуска до вызова средства удаления. В случаях, когда один поток увеличивает значение refcount, а другой уменьшает его, вы можете доказать, что единственный действительный способ сделать это — синхронизировать потоки с друг другом, чтобы деструктор увидел результат обоих потоков. Несоблюдение синхронизации может привести к гонке в любом случае, поэтому пользователь обязан сделать это правильно.

1

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

После размышлений над № 1 я был убежден, что они не эквивалентны этому аргументу §29.8.3 в [atomics.fences]:

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

Этот параграф говорит, что релиз забор может быть синхронизирован только с операцией aquire. Но релиз операция может быть дополнительно синхронизирован с операцией потребления.

0

Ваша пустота read_y_then_x () с забором забора имеет забор в неправильном месте. Он должен быть помещен между двумя атомными нагрузками. Забор заборов по существу делает всю нагрузку над забором чем-то похожим на сбор заборов, за исключением того, что раньше не было установлено, пока вы не выполнили забор.

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