Как работает memory_order_relaxed для увеличения количества атомных ссылок в интеллектуальных указателях?
Рассмотрим следующий фрагмент кода, взятый из выступления Херба Саттера об атомарности:
Класс 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, если refs равен N в некоторой точке, и два потока A и B выполняют следующий код:
control_block_ptr->refs.fetch_add(1, memory_order_relaxed);
тогда может случиться, что оба потока увидят, что значение ref равно 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);
Какое значение refs наблюдалось в Thread 2 перед вызовом fetch_add? Это может быть N или N+1? Какое значение refs наблюдается в Thread 2 после вызова fetch_add? Должно ли это быть как минимум N + 2?
[Обсуждение URL: C++ & Beyond 2012 - http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-2-of-2 (@ 1: 20:00)]
3 ответа
Boost.Atomic библиотека, которая эмулирует std::atomic
предоставляет аналогичный пример подсчета ссылок и объяснение, и это может помочь вашему пониманию.
Увеличение счетчика ссылок всегда можно сделать с помощью
memory_order_relaxed
: Новые ссылки на объект могут быть сформированы только из существующей ссылки, и передача существующей ссылки из одного потока в другой уже должна обеспечивать любую необходимую синхронизацию.Важно обеспечить любой возможный доступ к объекту в одном потоке (через существующую ссылку) перед удалением объекта в другом потоке. Это достигается с помощью операции "release" после удаления ссылки (любой доступ к объекту через эту ссылку, очевидно, должен происходить раньше) и операции "receive" перед удалением объекта.
Можно было бы использовать
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.