Здравствуйте, Alexander G, Вы писали:
AG>Между прочим, в узком смысле. Одновременный reset и get из разных нитей недопустим.
Он настолько же потоко-безопасный как и std::string. Это называется basic thread-safety, т.е. для одного объекта можно одновременно вызывать константные методы, можно одновременно вызывать не константные методы для разных объектов.
AG>Было бы интересно на полностью потокобезопасные умные указатели на атоомарных операциях.
"Полностью" потоко-безопасные указатели (для которых можно одновременно делать reset() и get()) называются with strong thread-safety.
Ты наверное имел в виду не на атомарных операциях, а lock/wait-free. Потому что на атомарных операциях легко сымитировать спин-мьютекс, и дальше...
lock/wait-free алгоритмы (точнее алгоритм, по большому счёту он один) подсчёта ссылок со strong thread-safety основаны на т.н. дифференциальном подсчёте ссылок. Т.е. есть 2 счётчика: первый — в объекте как обычно, а второй — внутри умного указателя рядом с указателем на объект. На первый идут операции декремента, а на второй — операции инкремента. Когда умный указатель разрушается или изменяет значение его счётчик переносится в объект. Соотв. тут уже получается 2 типа умных указателей — условно atomic_ptr и shared_ptr (или более правильно было бы назвать их shared_ptr и local_ptr), у первого можно любые операции одновременно дёргать, т.е. подразумевается, что он лежит в каком-то разделяемом общедоступном месте (допустим глобальная переменная atomic_ptr<settings_t> g_current_settings). Второй — как обычный std::string (или boost::shared_ptr), т.е. локальный для потока.
Основная хитрость в том, что когда поток захватывает разделяемый указатель он делает это атомарно (одновременно увеличивает счётчик и считывает значение указателя, для которого он увеличил счётчик):
Когда разделяемый указатель изменяется, то это тоже происходит атомарно за одну операцию:
void reset(atomic_ptr* ptr, T* n)
{
atomic_ptr xchg = {n, 0};
atomic_ptr tmp = ATOMIC_XCHG(ptr, xchg);
// some manipulations with tmp to correctly handle
// reference count for previous object
}
Первая (и насколько мне известно единственная) реализация алгоритма в виде полноценных умных указателей — это atomic-ptr-plus: http://atomic-ptr-plus.sourceforge.net/
Несколько лет назад я выкладывал тут компонент, который использует схожий, но несколько улучшенный алгоритм (он wait-free, в отличии от atomic-ptr-plus, хотя там есть свои тонкости). Фактически это умный указатель, только сильно ограниченный в использовании: может быть только один разделяемый указатель на объект, локальные указатели нельзя копировать. Т.е. он предназначен именно для ситуации "atomic_ptr<settings_t> g_current_settings", а рабочие потоки захватывают текущие настройки на время обработки транзакции, работают с ними, потом отпускают; а глобальная переменная g_current_settings может в любой момент изменяться.
Тут Store — это есть atomic_ptr, а Handle — shared_ptr: http://rsdn.ru/forum/cpp/1922573.1.aspx
Занятный момент, что в современных языках со сборкой мусора (Java, C#), обычные "указатели" (объектные ссылки) полностью атомарные. Т.е. этой проблемы вообще нет, пожалуйста размещай указатель в разделяемом месте, одни потоки будут его периодически менять, другие считывать. Это будет замечательно работать, и очень дёшево: что захват, что изменение указателя — обычные инструкции загрузки/сохранения.
R>Он настолько же потоко-безопасный как и std::string. Это называется basic thread-safety, т.е. для одного объекта можно одновременно вызывать константные методы, можно одновременно вызывать не константные методы для разных объектов. AG>>Было бы интересно на полностью потокобезопасные умные указатели на атоомарных операциях.
А какой смысл в полностью потокобезопасных смартпоинтерах? Если будут работать 2 потока с _одним_ и тем же экземпляром смартпоинтера, даже если он сам по себе будет абсолютно потокобезопасным, сложно себе придумать код, использующий его таким образом, и не требующий синхронизации "поверх". Так как если другой поток вдруг поменяет содержимое смартпоинтера (скормив в reset ему новый инстанс объекта), а первый будет находится в коде обращения к объекту — он с успехом свалится. Чтоб этого избежать потребуется накрывать синхронизацией всю работу с таким смартпоинтеров. Таким образом strong потокобезопаснасть смартпоинтера является ненужным излишеством.
ЗЫ а че форум так расколбасило?
ЗЗЫ а че анонимов обидели?
Как много веселых ребят, и все делают велосипед...
Re[2]: между прочим boost::shared_ptr thread-safe'ный
Здравствуйте, ononim, Вы писали:
R>>Он настолько же потоко-безопасный как и std::string. Это называется basic thread-safety, т.е. для одного объекта можно одновременно вызывать константные методы, можно одновременно вызывать не константные методы для разных объектов. AG>>>Было бы интересно на полностью потокобезопасные умные указатели на атоомарных операциях.
O>А какой смысл в полностью потокобезопасных смартпоинтерах? Если будут работать 2 потока с _одним_ и тем же экземпляром смартпоинтера, даже если он сам по себе будет абсолютно потокобезопасным, сложно себе придумать код, использующий его таким образом, и не требующий синхронизации "поверх". Так как если другой поток вдруг поменяет содержимое смартпоинтера (скормив в reset ему новый инстанс объекта), а первый будет находится в коде обращения к объекту — он с успехом свалится. Чтоб этого избежать потребуется накрывать синхронизацией всю работу с таким смартпоинтеров. Таким образом strong потокобезопаснасть смартпоинтера является ненужным излишеством.
Хмм... придётся начать с основ. Как работает управление временем жизни на основе подсчёта ссылок: если поток захватывает для себя ссылку на объект, то пока он её не отпустит объект не удаляется... естественно при условии корректной реализации подсчёта ссылок. Соотв. в твоём примере первый поток НЕ свалится, а будут себе преспокойно продолжать работать.
Вот, наверное, основной юз-кейс для указателя со strong thread-safety, заметьте, что он НЕ требует никакой дополнительной внешней синхронизации:
atomic_ptr<settings_t> g_settings;
atomic_ptr<routing_table_t> g_routing_table;
atomic_ptr<db_cache_t> g_db_cache;
void worker_thread()
{
while (request_t* req = get_next_request())
{
local_ptr<settings_t> settings (g_settings);
local_ptr<routing_table_t> routing_table (g_routing_table);
local_ptr<db_cache_t> db_cache (g_db_cache);
// process req using settings, routing_table and db_cache
}
}
Параллельно и асинхронно с этим настройки, рутинговая таблица и кэш БД могут изменяться (точнее сказать подменяться на новые). Подмена может происходить с разной частотой, и по разным причиным. Но суть в том, что она нисколько не влияет (не тормозит) основную обработку, обработчики просто захватывают наиболее последнии версии необходимых объектов и производят обработку, используя их, потом отпускают. Когда последняя ссылка на старый объект уходит, то он удаляется.
rw_mutex такой гибкости не даст, он будет требовать полной остановки всех рабочих потоков на время замены объекта, и что возможно более важно — на время остановки потоков. В смысле что потоки будут простаивать не только те 10 машинных инструкций пока непосредственно происходит подмена, а вначале затормозится один поток, потом через 1 мс ещё несколько, а последний поток может вообще сейчас вытеснен и не выполняется, всем остальным придётся подождать пока он будет поставлен на выполнение и наконец не освободит мьютекс. После этого заменяющий поток будет поставлен на выполнение, наконец сделает реальную подмену, и поставит рабочие потоки на выполнение, потом через некоторое время они все вернуться к нормальной работе. В итоге эффективное время простоя может быть существенным.
С другой стороны, конечно, rw_mutex не будет требовать временно хранить 2 копии объекта — текущую и предыдущую.
R>Хмм... придётся начать с основ. Как работает управление временем жизни на основе подсчёта ссылок: если поток захватывает для себя ссылку на объект, то пока он её не отпустит объект не удаляется... естественно при условии корректной реализации подсчёта ссылок. Соотв. в твоём примере первый поток НЕ свалится, а будут себе преспокойно продолжать работать.
R>Вот, наверное, основной юз-кейс для указателя со strong thread-safety, заметьте, что он НЕ требует никакой дополнительной внешней синхронизации: R>
Это НЕКОРРЕКТНЫЙ пример. Такой код не требует strong thread safety и прекрасно будет работать с boost::shared_ptr. Так что все ваши рассуждения про основы идут лесом.
Здравствуйте, ononim, Вы писали:
O>Это НЕКОРРЕКТНЫЙ пример. Такой код не требует strong thread safety и прекрасно будет работать с boost::shared_ptr. Так что все ваши рассуждения про основы идут лесом.
Товарищь, ononim, вы бы не спешили рубить с плеча, а лучше бы прочитали дальше:
Параллельно и асинхронно с этим настройки, рутинговая таблица и кэш БД могут изменяться (точнее сказать подменяться на новые). Подмена может происходить с разной частотой, и по разным причиным. Но суть в том, что она нисколько не влияет (не тормозит) основную обработку, обработчики просто захватывают наиболее последнии версии необходимых объектов и производят обработку, используя их, потом отпускают. Когда последняя ссылка на старый объект уходит, то он удаляется.
Т.е. часть потоков копирует разделяемый умный указатель, часть делает ему reset(). C boost::shared_ptr<> это работать НЕ будет.
Здравствуйте, remark, Вы писали:
R>Здравствуйте, ononim, Вы писали:
O>>Это НЕКОРРЕКТНЫЙ пример. Такой код не требует strong thread safety и прекрасно будет работать с boost::shared_ptr. Так что все ваши рассуждения про основы идут лесом.
R>Товарищь, ononim, вы бы не спешили рубить с плеча, а лучше бы прочитали дальше: R>
R>Параллельно и асинхронно с этим настройки, рутинговая таблица и кэш БД могут изменяться (точнее сказать подменяться на новые). Подмена может происходить с разной частотой, и по разным причиным. Но суть в том, что она нисколько не влияет (не тормозит) основную обработку, обработчики просто захватывают наиболее последнии версии необходимых объектов и производят обработку, используя их, потом отпускают. Когда последняя ссылка на старый объект уходит, то он удаляется.
R>Т.е. часть потоков копирует разделяемый умный указатель, часть делает ему reset(). C boost::shared_ptr<> это работать НЕ будет.
Согласен. Но в вашем коде тут — http://rsdn.ru/forum/cpp/3521564.1.aspx
свалится, несмотря ни на какой супер-пупер-потокобезопасный atomic_ptr, надеюсь не надо объяснять почему.
O>>но кроме академичности, он ничем не лучше варианта с boost::shared_ptr который написали вы, поставив почемуто там atomic_ptr R>Я поставил там atomic_ptr, потому что с boost::shared_ptr код работать не будет.
Будет. Именно тот код что вы привели в http://rsdn.ru/forum/cpp/3521564.1.aspx
Здравствуйте, ononim, Вы писали:
R>>Т.е. часть потоков копирует разделяемый умный указатель, часть делает ему reset(). C boost::shared_ptr<> это работать НЕ будет. O>Согласен. Но в вашем коде тут — http://rsdn.ru/forum/cpp/3521564.1.aspx
O>свалится, несмотря ни на какой супер-пупер-потокобезопасный atomic_ptr, надеюсь не надо объяснять почему.
Любая автоматическая система управлением временем жизни не удаляет объекты пока они используются. Точка.
Если происходит обратное, то либо (1) в системе есть банальная ошибка, (2) её некорректно используют.
По поводу выше-обозначенного примера нельзя определенно сказать, пока мы не определимся о каком именно atomic_ptr идёт речь. Но я могу предположить 3 варианта:
1. atomic_ptr поддерживает прямые обращения к объекту. Соотв. operator->() возвращает local_ptr, который держит объект, пока не выполнится foo(). Диагноз: всё прекрасно работает.
2. atomic_ptr просто не поддерживает прямые обращения к объекту, и заставляет пользователя вначале явно создавать local_ptr. Диагноз: такой код не скомпилируется.
3. atomic_ptr криво спроектирован и/или реализован. Диагноз: при таком предположении любой код может валиться.
O>>>но кроме академичности, он ничем не лучше варианта с boost::shared_ptr который написали вы, поставив почемуто там atomic_ptr R>>Я поставил там atomic_ptr, потому что с boost::shared_ptr код работать не будет. O>Будет. Именно тот код что вы привели в http://rsdn.ru/forum/cpp/3521564.1.aspx
прекраснейшим образом будет работать на boost::shared_ptr.
Хорошо, считаем, что произошло некоторое недопонимание из-за того, что я часть кода описал словами. Полный код вверху этого поста.
Теперь вопрос по поводу того, что подсчёт ссылок с сильной потоко-безопасностью пустое излишество, и что при использовании смарт поинтеров нужна дополнительная синхронизацией снимается?
Здравствуйте, remark, Вы писали:
AG>>Было бы интересно на полностью потокобезопасные умные указатели на атоомарных операциях.
На всякий случай упомяну, что "не на атомарных операциях" это решается как boost::shared_ptr + boost::mutex в одном флаконе. Т.е. тупо все операции над умным указателем защищаем мьютексом.
И ещё сюда же, С++0х std::shared_ptr будет поддерживать сильную потоко-безопасность. Насколько помню через перегрузку функций std::atomic_load(). Однако тип указателя один и для разделяемых переменных и для локальных переменных.
R>>>Т.е. часть потоков копирует разделяемый умный указатель, часть делает ему reset(). C boost::shared_ptr<> это работать НЕ будет. O>>Согласен. Но в вашем коде тут — http://rsdn.ru/forum/cpp/3521564.1.aspx
нету reset для g_settings. R>Потому что это не код, а пример состоящий из куска кода и текста вокруг него. R>Хорошо, вот полный пример:
Совсем некрасиво менять концептуально код, ссылаясь на то что мол "изначально мелкие детали опустил", причем новый вариант явно перегружая действительно мелкими деталями, помимо важной смены концепции.
R>>>... всё будет работать нормально. O>>Не будет:
O>>
O>>свалится, несмотря ни на какой супер-пупер-потокобезопасный atomic_ptr, надеюсь не надо объяснять почему.
R>Любая автоматическая система управлением временем жизни не удаляет объекты пока они используются. Точка. R>Если происходит обратное, то либо (1) в системе есть банальная ошибка, (2) её некорректно используют.
R>По поводу выше-обозначенного примера нельзя определенно сказать, пока мы не определимся о каком именно atomic_ptr идёт речь. Но я могу предположить 3 варианта: R>1. atomic_ptr поддерживает прямые обращения к объекту. Соотв. operator->() возвращает local_ptr, который держит объект, пока не выполнится foo(). Диагноз: всё прекрасно работает.
Замечательно, но не скомпилируется без явного тайпкаста от local_ptr к settings_t. Иначе откуда у local_ptr вырастет foo()? А если придется писать явные тайпасты при каждом вызове (представляете себе продакшн код?), либо модифицировать оборачиваемый класс (boost::intrusive_ptr тоже самое делает лучшим образом, позволяя реализовать и strong thread safety, благо там и синхронизировать то нечего) — это и есть академичность. Ну может моих познаний в С++ недостаточно чтобы сделать такой local_ptr. В таком случае просветите, и если оно скомпиляется — честно признаю поражение
R>2. atomic_ptr просто не поддерживает прямые обращения к объекту, и заставляет пользователя вначале явно создавать local_ptr. Диагноз: такой код не скомпилируется.
Вобщем-то это компилябельный вариант пункта 1
R>3. atomic_ptr криво спроектирован и/или реализован. Диагноз: при таком предположении любой код может валиться.
Это верно, но см 1.
Как много веселых ребят, и все делают велосипед...
Re[8]: между прочим boost::shared_ptr thread-safe'ный
ononim:
R>>1. atomic_ptr поддерживает прямые обращения к объекту. Соотв. operator->() возвращает local_ptr, который держит объект, пока не выполнится foo(). Диагноз: всё прекрасно работает. O>Замечательно, но не скомпилируется без явного тайпкаста от local_ptr к settings_t. Иначе откуда у local_ptr вырастет foo()?
R>>>1. atomic_ptr поддерживает прямые обращения к объекту. Соотв. operator->() возвращает local_ptr, который держит объект, пока не выполнится foo(). Диагноз: всё прекрасно работает. O>>Замечательно, но не скомпилируется без явного тайпкаста от local_ptr к settings_t. Иначе откуда у local_ptr вырастет foo()? НИ>Со стрелкой-то как раз всё очень просто:
ну чтож, согласен
Как много веселых ребят, и все делают велосипед...
Re[8]: между прочим boost::shared_ptr thread-safe'ный
Здравствуйте, ononim, Вы писали:
R>>>>Т.е. часть потоков копирует разделяемый умный указатель, часть делает ему reset(). C boost::shared_ptr<> это работать НЕ будет. O>>>Согласен. Но в вашем коде тут — http://rsdn.ru/forum/cpp/3521564.1.aspx
нету reset для g_settings. R>>Потому что это не код, а пример состоящий из куска кода и текста вокруг него. R>>Хорошо, вот полный пример:
O>Совсем некрасиво менять концептуально код, ссылаясь на то что мол "изначально мелкие детали опустил", причем новый вариант явно перегружая действительно мелкими деталями, помимо важной смены концепции.
Полностью согласен. Хорошо, что я этого не делал, и изначально написал "Параллельно и асинхронно с этим настройки, рутинговая таблица и кэш БД могут изменяться (точнее сказать подменяться на новые)".
R>>Любая автоматическая система управлением временем жизни не удаляет объекты пока они используются. Точка. R>>Если происходит обратное, то либо (1) в системе есть банальная ошибка, (2) её некорректно используют.
R>>По поводу выше-обозначенного примера нельзя определенно сказать, пока мы не определимся о каком именно atomic_ptr идёт речь. Но я могу предположить 3 варианта: R>>1. atomic_ptr поддерживает прямые обращения к объекту. Соотв. operator->() возвращает local_ptr, который держит объект, пока не выполнится foo(). Диагноз: всё прекрасно работает. O>Замечательно, но не скомпилируется без явного тайпкаста от local_ptr к settings_t. Иначе откуда у local_ptr вырастет foo()? А если придется писать явные тайпасты при каждом вызове (представляете себе продакшн код?), либо модифицировать оборачиваемый класс (boost::intrusive_ptr тоже самое делает лучшим образом, позволяя реализовать и strong thread safety, благо там и синхронизировать то нечего) — это и есть академичность. Ну может моих познаний в С++ недостаточно чтобы сделать такой local_ptr. В таком случае просветите, и если оно скомпиляется — честно признаю поражение
Если речь уже идёт о тонкостях и трюках С++, то предлагаю выносить в отдельную ветку. Я думаю, что тут достаточно людей смогут показать, как это сделать.
ononim,
Я, честно говоря, уже начинаю терять суть дискуссии.
Мой единственный тезис в этой ветке, что "Таким образом strong потокобезопаснасть смартпоинтера является ненужным излишеством" — это полный бред и неправда. Если не согласны, то я готов парировать конкретные аргументы.
Здравствуйте, remark, Вы писали:
R>"Полностью" потоко-безопасные указатели (для которых можно одновременно делать reset() и get()) называются with strong thread-safety. R>Ты наверное имел в виду не на атомарных операциях, а lock/wait-free. Потому что на атомарных операциях легко сымитировать спин-мьютекс, и дальше... R>lock/wait-free алгоритмы (точнее алгоритм, по большому счёту он один) подсчёта ссылок со strong thread-safety основаны на т.н. дифференциальном подсчёте ссылок. Т.е. есть 2 счётчика: первый — в объекте как обычно, а второй — внутри умного указателя рядом с указателем на объект. На первый идут операции декремента, а на второй — операции инкремента. Когда умный указатель разрушается или изменяет значение его счётчик переносится в объект. Соотв. тут уже получается 2 типа умных указателей — условно atomic_ptr и shared_ptr (или более правильно было бы назвать их shared_ptr и local_ptr), у первого можно любые операции одновременно дёргать, т.е. подразумевается, что он лежит в каком-то разделяемом общедоступном месте (допустим глобальная переменная atomic_ptr<settings_t> g_current_settings). Второй — как обычный std::string (или boost::shared_ptr), т.е. локальный для потока.
А два счётчика принципиально? Вроде бы QSharedPointer из Qt обещает быть thread-safe. Там два или не два?
Re[2]: между прочим boost::shared_ptr thread-safe'ный
Здравствуйте, D14, Вы писали:
D14>А два счётчика принципиально? Вроде бы QSharedPointer из Qt обещает быть thread-safe. Там два или не два?
Там один (ну точнее 2, но по другой причине — они поддерживают слабые ссылки, но это совсем другая история, для наших целей можно считать, что один).
Соотв. QSharedPointer ни разу не thread-safe, такой же thread-safe как и std::string.
А документация просто полная чушь:
QSharedPointer and QWeakPointer are thread-safe and operate atomically on the pointer value. Different threads can also access the same QSharedPointer or QWeakPointer object at the same time without need for locking mechanisms.
Смотри код:
inline void ref() const { d->weakref.ref(); d->strongref.ref(); }
inline void internalSet(Data *o, T *actual)
{
if (d == o) return;
if (o && !o->strongref)
o = 0;
if (o) {
verifyReconstruction(actual);
o->weakref.ref();
o->strongref.ref();
}
if (d && !deref())
delete d;
d = o;
this->value = d && d->strongref ? actual : 0;
}
Это тот же самый примитивный basic-thread safety atomic reference counting, что и в boost::shared_ptr.
А теперь представь, что один поток зашёл в ref(), считал значение d, и был прерван.
Пришёл второй поток, вызвал internalSet(), заменил объект на новый, а старый удалил.
Теперь просыпается первый поток, и пытается своему, уже удалённому, объекту вызвать d->weakref.ref().