Атомарность конструктора копирования объекта счетчика ссылок с использованием InterlockedIncrement64

Я пытаюсь понять, как я могу гарантировать, что счетчик ссылок на объекты является потокобезопасным.

class MyObject{
  //Other implementation details
private:
  mutable volatile LONGLONG * m_count;
  IData * m_data;
};

Предположим, что необходимое объявление класса есть, просто сохраняя простоту. Вот реализация конструктора копирования и деструктора.

MyObject::MyObject(const MyObject& rhs) : m_count(rhs.m_count), m_data(rhs.m_data){
    InterlockedIncrement64(m_count);
}

MyObject::~MyObject(){
    if(InterlockedDecrement64(m_count) == 0)
    delete m_data;
}

Эта ветка безопасна? Как виден список интилизации конструктора копирования, атомарный или нет? Это вообще имеет значение? Должен ли я устанавливать увеличенное значение счетчика в списке инициализации (возможно ли это)?

В нынешнем виде этого достаточно. Я думаю, что да, иначе как бы я мог попасть в сценарий, где thread1 копирует, а thread2 одновременно уничтожает, когда count == 1. Между потоками должно быть рукопожатие, то есть поток 1 должен полностью скопировать объект до того, как объект потока 2 выйдет из области видимости, верно?

Прочитав некоторые из этих ответов, я вернулся и провел небольшое исследование. Boost реализует их shared_ptr очень похоже. Вот вызов деструктора.

void release() // nothrow
{
    if( BOOST_INTERLOCKED_DECREMENT( &use_count_ ) == 0 )
    {
        dispose();
        weak_release();
    }
}

Некоторые предположили, что в документации по boost четко указано, что присваивание не является потокобезопасным. Я согласен и не согласен. Я думаю, что в моем случае я не согласен. Мне нужно только рукопожатие между threadA и threadB. Я не думаю, что некоторые из проблем, описанных в некоторых ответах, применимы здесь (хотя они были откровенными ответами, которые я не полностью обдумал).

Пример ThreadA atach(SharedObject); //Общий объект передается по значению, увеличивается счетчик и т. д. и т. д.

ThreadB //Принимает объект, добавляет его в список общих объектов. ThreadB находится на таймере, который уведомляет все SharedObjects о событии. Перед уведомлением делается копия списка, защищенная критической секцией. CS выпускается, копии уведомляются.

ThreadA отсоединить (общий объект); //Удаляет общий объект из списка объектов

Теперь одновременно ThreadB посылает SharedOjbect и уже сделал копию списка до того, как ThreadA отсоединил указанный общий объект. Все в порядке нет?


person clanmjc    schedule 14.09.2012    source источник
comment
Почему 64-битный счетчик ссылок? Вы уверены, что не переусердствуете с этим?   -  person    schedule 14.09.2012
comment
Вы можете взглянуть на std::atomic, если вы просто хотите защитить счетчик, или std::mutex, если вам нужна более широкая защита.   -  person Some programmer dude    schedule 14.09.2012
comment
Я не вижу проблем с кодом.   -  person David Schwartz    schedule 14.09.2012


Ответы (4)


Технически это должно быть безопасно.

Почему? Потому что для копирования объекта источник должен иметь «ссылку», поэтому он не исчезнет во время копирования. Кроме того, никто не имеет доступа к объекту, который в настоящее время находится в стадии строительства.

Деструктор тоже безопасен, так как ссылок все равно не остается.

Тем не менее, вы можете подумать о копировании счетчика ссылок. Эти ссылки на самом деле не существуют; каждый, у кого есть ссылка на оригинал, должен был бы каким-то образом уменьшить счетчик ссылок и для копии, при условии, что он получил исходную ссылку до того, как была сделана копия. Копии должны начинаться как новые объекты с числом ссылок равным 1.

РЕДАКТИРОВАТЬ: аналогично, если вы реализуете оператор присваивания (который похож на копию в существующий объект), счетчик ссылок целевого объекта следует оставить как есть.

person Christian Stieber    schedule 14.09.2012
comment
Вы не правы, ничто не мешает другому потоку удалить глобальный объект между списком инициализации и выполнением тела ctor. - person Rost; 14.09.2012
comment
@Rost: это будет другая ошибка, не связанная с этим кодом. Этот код безопасен до тех пор, пока в несвязанном коде нет несвязанных ошибок. Если вы передаете ссылку на объект другой функции, вы несете ответственность за то, чтобы объект существовал до тех пор, пока эта функция не вернется. (В противном случае ни одна функция-член никогда не будет потокобезопасной, поскольку объект может быть уничтожен во время выполнения этой функции, то есть UB.) Его коду не нужно исправлять ошибки в вызывающих программах, и он не может этого делать. - person David Schwartz; 14.09.2012
comment
@DavidSchwartz Не с точки зрения безопасности потоков. И это не баг, это ожидаемое поведение — иначе нет необходимости использовать атомарность. - person Rost; 14.09.2012
comment
@Rost: Atomics необходимы, потому что другой поток может увеличивать или уменьшать счетчик ссылок, пока эта функция увеличивает его. Однако он не может сбросить его до нуля, потому что вызывающий объект содержит ссылку. - person David Schwartz; 14.09.2012
comment
Я согласен с Ростом, это небезопасно. Ссылки — это волшебные вещи, которые предотвращают удаление чего-либо. Даже в однопоточном приложении довольно легко иметь ссылку на то, чего больше не существует. Неопределенное поведение, конечно, но его легко вызвать. Если волшебство не работает в однопоточном приложении, почему вы думаете, что оно вдруг сработает в многопоточном приложении? - person David Hammen; 14.09.2012
comment
@DavidHammen: Функция, которая принимает ссылку, не несет ответственности за то, чтобы вызывающая сторона вызывала ее разумно. Каждая функция, принимающая ссылку, требует, чтобы вызывающая сторона гарантировала, что объект не будет уничтожен во время выполнения функции. - person David Schwartz; 14.09.2012

Конструктор не безопасен, т.к. список инициализации не атомарный (упоминаний об этом в Стандарте я не нашел, но вряд ли это будет реализовано).

Поэтому, если другой поток удалит объект, который в настоящее время копируется текущим потоком - прямо между выполнением списка инициализации и выполнением InterlockedIncrement() - вы получите сломанные (уже удаленные m_data) m_data и m_count. Это приведет как минимум к двойному удалению m_data.

Помещение InterlockedIncrement в список инициализаторов не поможет, потому что переключение потоков может произойти после вызова ctor, но до инициализации m_count.

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

person Rost    schedule 14.09.2012
comment
Другой поток не может удалить объект, так как этот поток содержит на него ссылку. Какая бы функция ни вызывала эту функцию, она вызывает функцию-член для этого объекта, поэтому она должна содержать ссылку на него. Ответственность за то, чтобы объект продолжал существовать, пока он работает с объектом, лежит на вызывающем объекте. По этой логике никакая функция-член никогда не является безопасной, поскольку объект всегда может быть уничтожен во время выполнения этой функции-члена. - person David Schwartz; 14.09.2012
comment
@DavidSchwartz Точно. Никакая функция не является безопасной, если несколько потоков имеют доступ к объекту и могут его удалить. Ссылка на объект не имеет значения в данном случае, это просто указатель, переданный стеком. - person Rost; 14.09.2012
comment
Нет. Функция безопасна, даже если несколько потоков имеют доступ к объекту и могут удалить его, потому что функция не несет ответственности за предотвращение этого. Если вызывающие абоненты позволяют это, вызывающие абоненты небезопасны. Если бы мы приняли вашу логику, никакие функции не были бы безопасными, и в этом случае нет причин критиковать его код за небезопасность, поскольку весь код должен быть небезопасным. Даже глобальная блокировка не поможет, так как какой-нибудь другой поток может удалить блокировку, как раз в тот момент, когда он ее получает. - person David Schwartz; 14.09.2012
comment
Справа: если исходный объект находится в потоке, отличном от этого конструктора, появляется короткое окно после того, как этот конструктор скопирует значение указателя и до того, как он увеличит счетчик ссылок, когда переключение потока может привести к тому, что деструктор для исходного объекта уменьшит ссылку считать до 0 и уничтожать управляемый объект. - person Pete Becker; 14.09.2012
comment
@PeteBecker: Этот аргумент одинаково хорошо применим к любой функции-члену любого объекта. Конечно, каждая функция-член каждого объекта (и каждая функция, принимающая ссылку на объект) небезопасна, если базовые объекты удаляются в другом потоке во время выполнения этой функции. Ответственность за то, чтобы этого не произошло, лежит на вызывающем объекте, сохраняя действительную ссылку на базовые объекты во время выполнения функции. - person David Schwartz; 14.09.2012
comment
@DavidSchwartz - может быть, вообще. Но когда вы пишете класс-оболочку, целью которого является избежать такого рода проблем, вы не можете предположить, что такого рода проблем не возникнет. Это потокобезопасно, если вы не облажаетесь, это то же самое, что и не потокобезопасно. - person Pete Becker; 14.09.2012
comment
@PeteBecker: я не могу понять, как это применимо в данном случае. Функция принимает ссылку, как и все конструкторы копирования. Вы говорите, что конструктор копирования не может быть потокобезопасным? - person David Schwartz; 14.09.2012
comment
+1, не безопасно. Тот факт, что тест if является атомарным, не делает весь блок if атомарным. Это программист, а не C++, должен сделать весь блок атомарным. - person David Hammen; 14.09.2012
comment
@DavidHammen: блок if не обязательно должен быть атомарным. Ему просто нужно, чтобы операция возвращала true в одном и только одном потоке после завершения каждого потока с использованием объекта, защищенного счетчиком. - person David Schwartz; 14.09.2012
comment
@DavidSchwartz - конструкторы копирования в целом не являются потокобезопасными именно потому, что, когда копируемый объект находится в потоке, отличном от потока, в котором делается копия, исходный объект может быть уничтожен до создание копии завершено. В общем, решение этой проблемы - не делайте этого. Но когда целью класса является обеспечение потокобезопасности, не делайте этого, это означает, что он потерпел неудачу. - person Pete Becker; 14.09.2012
comment
@DavidSchwartz Да, именно поэтому многопоточное программирование такое сложное. См., например. boost::shared_ptr требования к безопасности потоков. В нем четко указано, что оператор присваивания не является потокобезопасным. Очень похоже на этот случай. - person Rost; 14.09.2012
comment
@PeteBecker: исходный объект не может быть уничтожен до завершения конструктора, потому что вызывающая функция, которая явно является тем же потоком, содержит ссылку на него. В противном случае он не мог бы вызвать конструктор копирования. Рост: В вашей ссылке сказано, что объекты shared_ptr обеспечивают тот же уровень безопасности потоков, что и встроенные типы, и я не могу найти раздел, на который вы ссылаетесь, но я подозреваю, что вы его неправильно понимаете. - person David Schwartz; 14.09.2012
comment
@DavidSchwartz См. пример 3 по ссылке выше: //--- Пример 3 --- // thread A p = p3; // читает p3, записывает p // поток B p3.reset(); // пишет p3; не определено, одновременное чтение/запись - person Rost; 15.09.2012
comment
@Rost: это тот случай, когда два потока имеют одинаковую ссылку. Это не имеет отношения к этому коду и не применялось бы, если бы каждый поток имел свою собственную ссылку даже на один и тот же базовый объект. Конечно, вы не можете использовать ссылку для разных путей кода. Весь смысл ссылок в том, что каждый путь кода может иметь свой собственный для совместного использования базового объекта. Если бы вы могли просто поделиться объектом напрямую, вам не понадобились бы ссылки. (И это не имеет ничего общего с атомарностью или какими-либо проблемами, о которых идет речь в этом вопросе.) - person David Schwartz; 15.09.2012
comment
@DavidSchwartz Это не имеет смысла. Это случай, когда один поток копирует объект, принадлежащий второму потоку - точно такой же случай, как у нас. Пример 3 — это абсолютно тот же случай, просто речь идет о присваивании, а не о копировании. И почему я не могу поделиться ссылками (что бы вы под этим ни подразумевали)? Нет ничего, что могло бы предотвратить это, если ссылочный объект является частью общего состояния. - person Rost; 15.09.2012
comment
@Rost: поток ничего не может сделать с объектом, на который он не ссылается. Пока что-то содержит ссылку на объект, этот объект не может исчезнуть. Это действительно настолько просто. Например, для применения 3 два потока должны работать не только с одним и тем же объектом, но и с одной и той же ссылкой. В этом примере p3 — это общий указатель, который является ссылкой. - person David Schwartz; 15.09.2012
comment
@DavidSchwartz C ++ не имеет встроенного механизма для обеспечения этого, вы должны позаботиться об этом и сделать это вручную, используя некоторую синхронизацию потоков. Таким образом, использование ссылок C++ не является потокобезопасным. Итак, возвращаясь к теме, предлагаемый код не является потокобезопасным. - person Rost; 15.09.2012
comment
@Rost: Безопасен ли поток класса string C++ STL? Точно такие же требования и гарантии. Самое главное, этот код делает время жизни базовых объектов строго потокобезопасным. - person David Schwartz; 15.09.2012
comment
@DavidSchwartz Нет, обычно это не так. Вы не можете перевести его в общее состояние и делать все, что хотите, без синхронизации. То же самое с любым встроенным типом — int, double и т. д. Некоторые операции с ними являются потокобезопасными, а другие — нет, поэтому, как правило, они не являются потокобезопасными. - person Rost; 15.09.2012
comment
@Rost: Может быть, мы не согласны с тем, что означает потокобезопасность. Но если вы хотите утверждать, что ничто не является потокобезопасным, даже целые числа, тогда бесполезно описывать что-то как не потокобезопасное. Я согласен с тем, что в некотором смысле потоки могут неправильно использовать что угодно и, следовательно, в некотором смысле не являются потокобезопасными, но это не очень полезный смысл. И особенно бесполезно описывать какой-то конкретный кусок кода как не потокобезопасный. - person David Schwartz; 16.09.2012
comment
@DavidSchwartz Я полагаю, вы неправильно понимаете концепцию безопасности потоков. Любой конкретный фрагмент кода является потокобезопасным, если он использует только объекты в неразделяемых областях — стек, TLS, куча, специфичная для потока (или использует синхронизацию потоков). Например. если мы изменим обсуждаемую подпись ctor, чтобы передать rhs по значению: MyObject::MyObject(const MyObject rhs) - тогда этот ctor становится потокобезопасным. Ну, это совершенно бесполезно и не пример из реального мира, а просто для иллюстрации концепции. - person Rost; 16.09.2012

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

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

Это предполагает, что все ваши взаимосвязанные операции имеют полные барьеры.

как я мог попасть в сценарий, в котором thread1 копирует, а thread2 одновременно уничтожает, когда count == 1. Между потоками должно быть рукопожатие, что означает, что thread1 должен полностью скопировать объект до того, как объект thread2 выйдет из области действия правильно?

Вы не можете, пока каждый поток имеет свою собственную ссылку на объект. Объект не может быть уничтожен во время копирования thread1, если thread1 имеет собственную ссылку на объект. Thread1 не нужно копировать до того, как ссылка thread2 исчезнет, ​​потому что вы никогда, никогда не коснетесь объекта, если у вас нет ссылки на него.

Отдельные ссылки имеют слабую безопасность потоков, и к ним не следует обращаться одновременно в разных потоках. Если два потока хотят получить доступ к одному и тому же объекту, каждый из них должен иметь свою собственную ссылку. При передаче ссылки на объект в какой-либо другой код (возможно, в другой поток) выполните следующую последовательность операций:

  1. Иметь собственную ссылку.

  2. Создайте ссылку для другого кода из вашей собственной ссылки.

  3. Теперь вы можете уничтожить свою ссылку или отдать другую ссылку.

person David Schwartz    schedule 14.09.2012
comment
Если я передал ссылку на Object в другой поток, как долго я должен хранить исходный объект, чтобы гарантировать, что он не будет уничтожен во время создания копии в другом потоке? - person Pete Becker; 14.09.2012
comment
@PeteBecker: код, создающий ссылку на другой поток, может уничтожить свою собственную ссылку, как только завершит создание новой ссылки. 1) Иметь ссылку. 2) Создать ссылку на другой поток. 3) Уничтожить собственную ссылку. - person David Schwartz; 14.09.2012
comment
@PeteBecker: На самом деле существует только одно правило: Любой код, работающий с объектом, несет ответственность за то, чтобы он содержал ссылку на этот объект, пока он работает с этим объектом. Чтобы передать ссылка, вы должны создать ссылку для передачи, которая является операцией над объектом. Таким образом, вы должны удерживать ссылку до тех пор, пока не закончите создание ссылки, которую вы передадите другому потоку. - person David Schwartz; 14.09.2012

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

Но вы можете безопасно использовать свой класс, если никогда не используете new/delete, а работаете только с объектами, созданными и уничтоженными автоматически (по области действия).

person Alex Cohn    schedule 14.09.2012
comment
Объяснение было бы полезно. - person David Schwartz; 15.09.2012