Как работает 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, если рефов равно 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)]

9

Решение

Boost.Atomic библиотека, которая эмулирует std::atomic обеспечивает аналогичный пример подсчета ссылок и объяснение, и это может помочь вашему пониманию.

Увеличение счетчика ссылок всегда можно сделать с помощью memory_order_relaxed: Новые ссылки на объект могут быть сформированы только из существующей ссылки, и передача существующей ссылки из одного потока в другой уже должна обеспечивать любую необходимую синхронизацию.

Важно обеспечить любой возможный доступ к объекту в одном потоке (через существующую ссылку), прежде чем удалять объект в другом потоке. Это достигается с помощью операции «освобождение» после удаления ссылки (любой доступ к объекту через эту ссылку, очевидно, должен происходить раньше) и операции «получение» перед удалением объекта.

Можно было бы использовать memory_order_acq_rel для операции fetch_sub, но это приводит к ненужным операциям «получения», когда счетчик ссылок еще не достигает нуля и может налагать снижение производительности.

5

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

С 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 атомарные операции могут быть переупорядочены вокруг текущей атомарной операции с указанным порядком памяти.

2

Поскольку это довольно запутанно (по крайней мере, для меня), я собираюсь частично затронуть один момент:

(…) тогда может случиться, что оба потока увидят, что значение ref равно N, и оба запишут в него N + 1 (…)

Согласно @AnthonyWilliams в этот ответ, вышеприведенное предложение представляется неправильным, так как:

Единственный способ гарантировать, что у вас есть «последнее» значение — это использовать операцию чтения-изменения-записи, такую ​​как exchange (), compare_exchange_strong () или fetch_add (). Операции чтения-изменения-записи имеют дополнительное ограничение, заключающееся в том, что они всегда работают с «последним» значением, поэтому последовательность операций ai.fetch_add (1) для ряда потоков будет возвращать последовательность значений без дубликатов или пробелов. В отсутствие дополнительных ограничений, все еще нет гарантии, какие потоки будут видеть, какие значения.

Итак, учитывая аргумент авторитета, я бы сказал, что невозможно, чтобы оба потока увидели значение N в N + 1,.

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