Рассмотрим следующий фрагмент кода, взятый из выступления Херба Саттера об атомарности:
Класс smart_ptr содержит объект pimpl с именем control_block_ptr, содержащий счетчик ссылок рефов.
// Thread A:
// smart_ptr copy ctor
smart_ptr(const smart_ptr& other) {
...
control_block_ptr = other->control_block_ptr;
control_block_ptr->refs.fetch_add(1, memory_order_relaxed);
...
}
// Thread D:
// smart_ptr destructor
~smart_ptr() {
if (control_block_ptr->refs.fetch_sub(1, memory_order_acq_rel) == 0) {
delete control_block_ptr;
}
}
Херб Саттер говорит, что приращение рефов в потоке A можно использовать memory_order_relaxed, потому что «никто не делает ничего, основываясь на действии». Теперь, как я понимаю memory_order_relaxed, если рефов равно N в некоторой точке, и два потока A и B выполняют следующий код:
control_block_ptr->refs.fetch_add(1, memory_order_relaxed);
тогда может случиться так, что оба потока увидят значение рефов быть N и оба записать N + 1 обратно к нему. Это явно не сработает, и memory_order_acq_rel следует использовать так же, как и с деструктором. Куда я иду не так?
EDIT1: рассмотрим следующий код.
atomic_int refs = N; // at time t0.
// [Thread 1]
refs.fetch_add(1, memory_order_relaxed); // at time t1.
// [Thread 2]
n = refs.load(memory_order_relaxed); // starting at time t2 > t1
refs.fetch_add(1, memory_order_relaxed);
n = refs.load(memory_order_relaxed);
Какова стоимость рефов заметил поток 2 перед вызовом fetch_add? Это может быть N или N + 1? Какое значение refs наблюдается в Thread 2 после вызова fetch_add? Должно ли это быть как минимум N + 2?
[Обсуждение URL: C ++ & После 2012 года — http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-2-of-2 (@ 1:20:00)]Boost.Atomic библиотека, которая эмулирует std::atomic
обеспечивает аналогичный пример подсчета ссылок и объяснение, и это может помочь вашему пониманию.
Увеличение счетчика ссылок всегда можно сделать с помощью
memory_order_relaxed
: Новые ссылки на объект могут быть сформированы только из существующей ссылки, и передача существующей ссылки из одного потока в другой уже должна обеспечивать любую необходимую синхронизацию.Важно обеспечить любой возможный доступ к объекту в одном потоке (через существующую ссылку), прежде чем удалять объект в другом потоке. Это достигается с помощью операции «освобождение» после удаления ссылки (любой доступ к объекту через эту ссылку, очевидно, должен происходить раньше) и операции «получение» перед удалением объекта.
Можно было бы использовать
memory_order_acq_rel
для операции fetch_sub, но это приводит к ненужным операциям «получения», когда счетчик ссылок еще не достигает нуля и может налагать снижение производительности.
С C ++ ссылка на std::memory_order
:
memory_order_relaxed: расслабленная операция: синхронизация отсутствует
или ограничения порядка, налагаемые на другие операции чтения или записи, только это
атомарность операции гарантирована
Есть также пример ниже на этой странице.
В общем, std::atomic::fetch_add()
все еще атомно, даже когда с std::memory_order_relaxed
следовательно, одновременно refs.fetch_add(1, std::memory_order_relaxed)
из 2 разных потоков всегда будет увеличиваться refs
на 2. Точка порядка памяти — это то, как другие неатомные или std::memory_order_relaxed
атомарные операции могут быть переупорядочены вокруг текущей атомарной операции с указанным порядком памяти.
Поскольку это довольно запутанно (по крайней мере, для меня), я собираюсь частично затронуть один момент:
(…) тогда может случиться, что оба потока увидят, что значение ref равно N, и оба запишут в него N + 1 (…)
Согласно @AnthonyWilliams в этот ответ, вышеприведенное предложение представляется неправильным, так как:
Единственный способ гарантировать, что у вас есть «последнее» значение — это использовать операцию чтения-изменения-записи, такую как exchange (), compare_exchange_strong () или fetch_add (). Операции чтения-изменения-записи имеют дополнительное ограничение, заключающееся в том, что они всегда работают с «последним» значением, поэтому последовательность операций ai.fetch_add (1) для ряда потоков будет возвращать последовательность значений без дубликатов или пробелов. В отсутствие дополнительных ограничений, все еще нет гарантии, какие потоки будут видеть, какие значения.
Итак, учитывая аргумент авторитета, я бы сказал, что невозможно, чтобы оба потока увидели значение N в N + 1,.