Путаница с ошибкой реализации в деструкторе shared_ptr

Я только что видел выступление Херба Саттера: C ++ и после 2012 года: Херб Саттер — атомная<> Оружие, 2 из 2

Он показывает ошибку в реализации деструктора std :: shared_ptr:

if( control_block_ptr->refs.fetch_sub(1, memory_order_relaxed ) == 0 )
delete control_block_ptr; // B

Он говорит, что из-за memory_order_relaxed, удаление может быть помещено перед fetch_sub.

В 1:25:18 — Релиз не держит строку B ниже, где она должна быть

Как это возможно? Существует отношение «происходит до / последовательно», поскольку они оба находятся в одном потоке. Возможно, я ошибаюсь, но между fetch_sub и delete существует также перенос-зависимость.

Если он прав, какие пункты ISO поддерживают это?

9

Решение

Представьте себе код, который освобождает общий указатель:

auto tmp = &(the_ptr->a);
*tmp = 10;
the_ptr.dec_ref();

Если у dec_ref () нет семантики «release», то вполне нормально, чтобы компилятор (или процессор) перемещал вещи от до dec_ref () к нему (например):

auto tmp = &(the_ptr->a);
the_ptr.dec_ref();
*tmp = 10;

И это небезопасно, так как dec_ref () также может быть вызван из другого потока в то же время и удалить объект.
Таким образом, он должен иметь семантику «release», чтобы вещи до dec_ref () оставались там.

Теперь давайте представим, что деструктор объекта выглядит так:

~object() {
auto xxx = a;
printf("%i\n", xxx);
}

Также мы немного изменим пример и будем иметь 2 потока:

// thread 1
auto tmp = &(the_ptr->a);
*tmp = 10;
the_ptr.dec_ref();

// thread 2
the_ptr.dec_ref();

Тогда «агрегированный» код будет выглядеть так:

// thread 1
auto tmp = &(the_ptr->a);
*tmp = 10;
{ // the_ptr.dec_ref();
if (0 == atomic_sub(...)) {
{ //~object()
auto xxx = a;
printf("%i\n", xxx);
}
}
}

// thread 2
{ // the_ptr.dec_ref();
if (0 == atomic_sub(...)) {
{ //~object()
auto xxx = a;
printf("%i\n", xxx);
}
}
}

Однако, если у нас есть только семантика «release» для atomic_sub (), этот код можно оптимизировать следующим образом:

// thread 2
auto xxx = the_ptr->a; // "auto xxx = a;" from destructor moved here
{ // the_ptr.dec_ref();
if (0 == atomic_sub(...)) {
{ //~object()
printf("%i\n", xxx);
}
}
}

Но в этом случае деструктор не всегда будет печатать последнее значение «a» (этот код больше не является свободным). Вот почему нам также нужно получить семантику для atomic_sub (или, строго говоря, нам нужен барьер получения, когда счетчик становится 0 после декремента).

2

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

В ток-шоу Херб memory_order_release не memory_order_relaxed, но расслабленным будет еще больше проблем.

Если не delete control_block_ptr доступ control_block_ptr->refs (чего, вероятно, нет), тогда атомарная операция не переносит зависимость для удаления. Операция удаления может не касаться какой-либо памяти в блоке управления, она может просто вернуть этот указатель на распределитель свободного хранилища.

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

0

Похоже, он говорит о синхронизации действий над самим разделяемым объектом, которые не отображаются в его блоках кода (и как результат — сбивают с толку).

Вот почему он положил acq_rel — потому что все действия над объектом должны происходить до его уничтожения, все в порядке.

Но я все еще не уверен, почему он говорит об обмене delete с fetch_sub,

0

Это поздний ответ.

Давайте начнем с этого простого типа:

struct foo
{
~foo() { std::cout << value; }
int value;
};

И мы будем использовать этот тип в shared_ptr, следующее:

void runs_in_separate_thread(std::shared_ptr<foo> my_ptr)
{
my_ptr->value = 5;
my_ptr.reset();
}

int main()
{
std::shared_ptr<foo> my_ptr(new foo);
std::async(std::launch::async, runs_in_separate_thread, my_ptr);
my_ptr.reset();
}

Два потока будут работать параллельно, оба совместно владеют foo объект.

С правильным shared_ptr реализация
(то есть один с memory_order_acq_rel), эта программа определила поведение.
Единственное значение, которое эта программа напечатает, 5,

С неправильной реализацией (используя memory_order_relaxed) там
нет таких гарантий. Поведение не определено, потому что гонка данных
foo::value вводится. Беда возникает только в тех случаях, когда деструктор
вызывается в основном потоке. С расслабленным порядком памяти, пишите
в foo::value в другом потоке может не распространяться на деструктор в основном потоке.
Значение, отличное от 5 может быть напечатан.

Так что же такое гонка данных? Что ж, проверьте определение и обратите внимание на последний пункт:

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

  • обе конфликтующие оценки являются атомарными операциями (см. std :: atomic)
  • одна из конфликтующих оценок происходит раньше другой (см. std :: memory_order)

В нашей программе один поток напишет в foo::value и одна нить будет
читать из foo::value, Они должны быть последовательными; запись
в foo::value всегда должно происходить до прочтения. Интуитивно это
имеет смысл, что они будут, как деструктор должен быть последним
вещь, которая происходит с объектом.

memory_order_relaxed не предлагает такие гарантии заказа, хотя и так memory_order_acq_rel необходимо.

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