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

Я снова и снова читал об атомных типах и операциях 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), видны (для другого потока, использующего получение), код выше * 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 at * 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 Почему после обнуления счетчика ссылок появляется заказ на приобретение? Мы просто читаем, что счетчик равен нулю и не используется никакая другая атомарная переменная (сам указатель не помечен / не используется как таковой).


person Community    schedule 27.06.2013    source источник


Ответы (3)


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

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

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

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

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

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

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

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

person Cort Ammon    schedule 12.09.2013

1

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

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

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

person A. Mikhaylov    schedule 24.07.2013
comment
Хорошо ... извините за поздний ответ и спасибо за ваш ответ. Но я не понимаю, очень плохо. Думаю, мне нужно перейти к основам ... - person ; 15.08.2013

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

person briand    schedule 12.09.2013