Глава из книги "Современное программирование на C++" называется "В сто первый раз об интеллектуальных указателях". Все бы ничего, но книга была издана в 2001 году, так стоит ли в очередной раз возвращаться к этой теме? Мне кажется что как раз сейчас и стоит. За эти пятнадцать лет поменялась сама точка зрения, тот угол под которым мы смотрим на проблему. В те далекие времена только-только вышла первая де-факто стандартная реализация — boost::shared_ptr<>, до этого каждый писал себе реализацию по потребности и как минимум представлял себе детали, сильные и слабые стороны своего кода. Все книги по C++ в то время обязательно описывали одну из вариаций умных указателей в мельчайших деталях.


Сейчас нам дан стандарт, и это хорошо. Но с другой стороны, уже не требуется понимать что там внутри, вместо этого достаточно три раза повторить мантру "используйте умные указатели везде где вы бы использовали обычные указатели", и это уже не так хорошо. Я подозреваю что далеко не все отдают себе отчет что данный стандарт — лишь один из возможных вариантов интерфейса, не говоря уже о разнице между реализациями различных вендоров. При выборе стандарта был сделан выбор между различными возможностями учитывающий разные факторы, но, оптимальный или нет, этот выбор очевидно не единственен.


А еще на stackoverflow например снова и снова задается вопрос — "потокобезопасны ли умные указатели из стандартной библиотеки?". Ответы даются обычно категоричные, но какие-то мало информативные. Если бы я например не знал о чем идет речь, то наверное бы не понял. И кстати, все сравнительно новые книги описывающие новый стандарт C++ этому вопросу тоже уделяют мало внимания.


Так давайте же попробуем сорвать покровы и разберемся с деталями.


Сразу определимся с терминологией, речь не идет о защите данных на которые ссылается указатель, это может быть обьект произвольной сложности и многопоточный доступ к нему требует в общем случае отдельной синхронизации. Под потокобезопасностью умного указателя мы подразумеваем защищенность собственно указателя на данные и валидности внутреннего счетчика ссылок. Образно говоря, если указатели создаются через std::make_shared<>() то никакие присвоения, передача в функции или другие потоки, свопы, уничтожение, не могут привести его в невалидное состояние. До тех пор пока мы не вызвали reset() или get(), мы вправе ожидать что указатель ссылается на некоторый валидный обьект, хотя и не обязательно тот на который мы подразумеваем.


Один из популярных ответов на вопрос заголовка: "It is only the control block itself which is thread-safe.". Вот мы и посмотрим, что понимается конкретно под control block и под safe.
Для экспериментов использовался g++-5.4.0.


Начнем с примера


Пусть в разделяемой памяти находится некоторая информация упакованная в структуру и доступная через указатель. Есть один или множество независимых потоков, которые должны читать и использовать эти данные без модификации, как правило оказывается что для них критически важна скорость доступа. В то же время, пусть существует один или несколько потоков модифицирующих эти данные с нарушением целостности, на практике обычно оказывается что модификации случаются значительно реже и скорость доступа там не настолько важна. Тем не менее, оставаясь в рамках классической (exclusive lock) синхронизации, мы вынуждены сериализовать доступ на чтение даже если если ни одного изменения данных не произошло. Естественно, на эффективности это отражается самым фатальным образом, и эта ситуация встречается настолько часто, возможно в несколько иной версии, что я бы ее рискнул назвать основным вопросом многопоточного программирования.


Конечно существуют стандартные решения, boost::shared_mutex и его юный отпрыск std::shared_mutex, позволяющие два уровня доступа — разделяемый на чтение и исключительный на запись. Однако же я, прослышав что std::shared_ptr дает потокобезопасный доступ к контрольному блоку (и не очень понимая что это значит), а так же что операции над ним реализованы lock-free, хочу предложить свое изящное решение:


// разделяемые данные
std::shared_ptr<SHARED_DATA> data;

reading_thread {
  // создаем резервную ссылку на разделяемые данные
  auto read_copy=data;
  // читаем данные, целостность гарантирована
  ...
  // деструктор read_copy
}

writing thread {
  // создаем измененную копию данных 
  auto update=std::make_shared<SHARED_DATA>(...args);
  // атомарно(?) обновляем данные
  data=update;
  // ссылка на старые данные теряется, но обьект будет уничтожен 
  //  в памяти только когда последний читатель закончит цикл чтения
}

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


Ну так что, сработает? Разумеется нет! Но почему именно?


Как оно устроено


Если взглянуть на саму структуру разделяемого указателя
/usr/include/c++/5/bits/shared_ptr_base.h: 1175


template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr
{
    ...
private:
    _Tp*           _M_ptr;         // Contained pointer.
    __shared_count<_Lp>  _M_refcount;    // Reference counter.
};

видно, что она состоит из двух членов — указателя на собственно данные и того самого контрольного блока. Но дело в том, что не существует способа атомарно и без блокирования поменять, присвоить, переместить и т.д. оба элемента. То есть разделяемые указатели потоко безопасными быть не могут(.?) Точка или вопросительный знак? Ну вроде бы точка, но какая-то расплывчатая, не окончательная, как-то это тривиально и слишком просто. Нам же было сказано что "потоко безопасен только доступ к контрольному блоку", а мы и не проверили.


Давайте разбираться, роем глубже


auto data=std::make_shared<int>(0);

void read_data() {
// имитация чтения, создается резервная копия данных
    for(;;)
        auto read_copy=data;
}

int main()
{
    std::thread(read_data).detach();
// имитация записи, весь указатель целиком заменяется другим
    for(;;)
        data=std::make_shared<int>(0);
    return 0;
}

Вот такой минималистский пример реализует предложенную идею и в общем оправдывает ожидания — валится с грохотом едва намотав несколько сотен циклов. Однако обратите внимание, к собственно данным мы здесь вообще ни разу не обращаемся, указатель не разыменовывается. То есть что-то неладное происходит с контрольным блоком? Зато у нас теперь есть код с которым можно работать дебаггером. Но сначала бросим взгляд на другие возможные варианты:


auto data=std::make_shared<int>(0);

void read_data() {
    for(;;)
        auto sp=std::atomic_load(&data);
}

int main(int argc, char**argv)
{
    std::thread(read_data).detach();
    for(;;) {
        std::atomic_exchange(&data, std::make_shared<int>(0));
        assert(std::atomic_is_lock_free(&data));
    }
    return 0;
}

здесь все прекрасно работаетло бы если бы не assert() в теле цикла. То есть атомарные операции над std::shared_ptr определены, но они блокирующие. Ну, это не наш путь, на мьютексах я и сам могу. Еще один вариант:


std::shared_ptr<int> variant[]={
    std::make_shared<int>(0),
    std::make_shared<int>(0)
};
auto data=variant[0];

void read_data() {
    for(;;)
        auto sp=data;
}

int main()
{
    std::thread(read_data).detach();
    for(size_t n=0;; ++n) {
        data=variant[n%2];
    }

    return 0;
}

почти идентичный, но он прекрасно работает полностью загружая два ядра под 100%. Разница в том что здесь один из потоков никогда не вызывает деструктор. Что же, деструкторы стандартных указателей небезопасны? Не верю. Давайте вернемся к первоначальному варианту и


Копнем еще поглубже


Рассмотрим поближе читающий поток:


    auto sp=data;

Здесь вызываются в цикле копирующий конструктор и деструктор, и все.


вот выжимки из исходного кода
//L1#shared_ptr_base.h : 662
      __shared_count(const __shared_count& __r) noexcept
      : _M_pi(__r._M_pi)
      {
    if (_M_pi != 0)
      _M_pi->_M_add_ref_copy();
      }
//L2#shared_ptr_base.h : 134
      void
      _M_add_ref_copy()
      { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }

//L1#shared_ptr_base.h : 658
     ~__shared_count() noexcept
      {
    if (_M_pi != nullptr)
      _M_pi->_M_release();
      }
//L2#shared_ptr_base.h : 147
    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
      {
            _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
        _M_dispose();
      }


которые, если отбросить все лишнее, сводятся к


// copy ctor
__gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1);
// old instance dtor
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
    _M_dispose();

или, если перейти к псевдокоду


++_M_pi->_M_use_count;
if(--_M_pi->_M_use_count == 0)
    dispose();

Здесь операторы инкремента и декремента подразумеваются атомарными, а функция dispose() очищает память и в частности инвалидирует сам указатель на счетчик ссылок _M_pi. Надо сказать что для привыкшего многопоточности выражение:


if(--cnt == 0)
do_something();


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


Тем не менее, что в это время происходит в другом, пишущем, потоке?


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

где-то вот так
//L1#shared_ptr.h : 291
      shared_ptr&
      operator=(shared_ptr&& __r) noexcept
      {
    this->__shared_ptr<_Tp>::operator=(std::move(__r));
    //L2#shared_ptr_base.h : 997
         __shared_ptr&
          operator=(__shared_ptr&& __r) noexcept
          {
        __shared_ptr(std::move(__r)).swap(*this);
        //L#3shared_ptr_base.h : 932
         __shared_ptr(__shared_ptr&& __r) noexcept
          : _M_ptr(__r._M_ptr), _M_refcount()
          {
        _M_refcount._M_swap(__r._M_refcount);
            //L#4shared_ptr_base.h : 684
             void _M_swap(__shared_count& __r) noexcept
              {
            _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
            __r._M_pi = _M_pi;
            _M_pi = __tmp;
              }
    //L2#shared_ptr_base.h : 1073
     void
      swap(__shared_ptr<_Tp, _Lp>& __other) noexcept
      {
    std::swap(_M_ptr, __other._M_ptr);
    _M_refcount._M_swap(__other._M_refcount);
      }
         //L3#shared_ptr_base.h : 684
          void
          _M_swap(__shared_count& __r) noexcept
          {
        _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
        __r._M_pi = _M_pi;
        _M_pi = __tmp;
          }
     //L2#shared_ptr_base.h : 658
      ~__shared_count() noexcept
      {
    if (_M_pi != nullptr)
      _M_pi->_M_release();
      }
         //L3#shared_ptr_base.h : 142
         void
          _M_release() noexcept
          {
            // Be race-detector-friendly.  For more info see bits/c++config.
            _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
        if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
          {
                _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
            _M_dispose();
    //L1#shared_ptr.h : 93 (destructor)
     //L2#shared_ptr_base.h : 658
      ~__shared_count() noexcept
      {
    if (_M_pi != nullptr) //_M_pi == nullptr - true here
      _M_pi->_M_release();
      }

Если отбросить все лишнее останутся примерно такие фрагменты кода:


    _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
    __r._M_pi = _M_pi;
    _M_pi = __tmp;
    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
         _M_dispose();

Опять же, первые три строчки (swap()) выглядят чрезвычайно подозрительными, однако при тщательном анализе они оказываются совершенно безопасными (естественно только в данном контексте) и все что нам остается (псевдокод).


if(--_M_pi->_M_use_count == 0)
    dispose();

то самое выражение, которое мы чуть выше сочли безопасным. Вот тут наступает момент истины:
// assert(_M_use_count == 1);


// writing thread // reading thread
if(--_M_pi->_M_use_count == 0) //true .
. ++_M_pi->_M_use_count; // count=1
. if(--_M_pi->_M_use_count == 0) //true
dispose(); // я первый! BANG!! dispose(); // нет я! BANG!!

Вот так комбинация атомарного инкремента и атомарного декремента приводит к гонке между потоками и std::shared_ptr<> не является потокобезопасным, даже на уровне контрольного блока. Вот теперь действительно точка.


Немного позже я нашел афористически краткое изложение этого принципа в документации к boost::shared_ptr<>. Звучит примерно так: "Указатели безопасны либо только на запись либо только на чтение, и небезопасны при конкурентном чтении/записи." Чуть развив эту мысль и сообразив что голая запись без чтения данных практически бессмысленна, мы видим что стандартные умные указатели безопасны на чтение, то есть настолько же насколько безопасны обычные константные указатели. То есть, вся эта сложная внутренняя машинерия создана для того чтобы достичь уровня обычных указателей, но не более, жирная точка.


Вместо заключения. От анализа к синтезу


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


Чего же нам не хватает для полноценной неблокирующей реализации? Существует очень небольшое число неблокирующих примитив работающих только со встроенными типами:


  • добавление целой константы (постфиксное или префиксное) — atomic_fetch_and_add и его вариации
  • обмен двух значений (на самом деле несимметричный, только один из операндов изменяется атомарно) — atomic_swap
  • условный обмен или присвоение с проверкой на равенство — atomic_compare_and_swap

Учитывая безусловный запрет на оператор if() в неблокирующих алгоритмах, только последний подходит на роль оператора ветвления, но его серьезнейшее ограничение в том что он позволяет проверку и присвоение исключительно одной и той же переменной. Вообще легко заметить нечто общее во всех трех примитивах — они атомарно работают с одной и только одной областью памяти размером с машинное слово, причины я думаю очевидны. Присмотревшись к обобщенной структуре разделяемого указателя, мы видим что он обязан содержать внутри себя как минимум один указатель на разделяемые данные (включающие контрольный блок) и где-то внутри этого блока должен находиться счетчик ссылок. При любых операциях с собственно указателем, например присвоении, мы должны атомарно проверять счетчик и одновременно изменять указатель на контрольный блок, что невозможно используя существующие атомарные примитивы. Отсюда следует что создание полноценного неблокирующего разделяемого указателя невозможно в принципе.


На самом деле я был бы рад ошибиться, возможно есть что-то, что я не знаю или понимаю неправильно? Всех желающих оспорить жду в комментариях.

Поделиться с друзьями
-->

Комментарии (52)


  1. babylon
    22.11.2016 03:23
    -1

    Использование goto в многопоточных системах принципиальный момент. Дейкстру — в топку!


  1. oYASo
    22.11.2016 03:27
    +1

    Спорить тут, в общем-то, не получится, потому что так оно и есть. В новом стандарте обещали запилить atomic_shared_ptr.


    1. degs
      22.11.2016 03:40

      В новом это в котором? Это не вот это?: /usr/include/c++/5.4.0/bits/shared_ptr_atomic.h
      Но он к сожалкнию блокирующий, маленькое лукавство — atomic не обязательно подразумевает non blocking.


      1. Duduka
        22.11.2016 09:28

        Это не лукавство, а разумность: с ростом конкуренции блокировки выигрывают у неблокирующих. Если пишешь на non blocking библиотеках, то должен учитывать, что с масштабированием есть небольшие проблемы, нужно оптимизировать приложения, уменьшая вероятность захвата ресурса.


        1. RPG18
          22.11.2016 11:39
          +1

          Если в системе много ядер, и куча потоков обращаются к атомарной переменной, то происходит блокировка на уровне инструкций процессора. Чуть более подробно в Современная операционная система: что надо знать разработчику


      1. oYASo
        23.11.2016 02:15

        Нет, не этот. По ссылке же черновик еще, в текущий компиляторах искать бесполезно. Я даже не уверен, что он появится в С++17, скорее на дальнюю перспективу.


  1. thatsme
    22.11.2016 09:37
    +6

    >> Ну, это не наш путь, на мьютексах я и сам могу
    А в чём проблема с мьютексами? Почему нужно без них? Это требование ТЗ, или желание в многопоточном приложении иметь 100% неблокируемый код? Если второе, то это невозможно при разделяемых объектах, и скорее относится к религиозному чем к рациональному.


    1. degs
      22.11.2016 15:39

      Постановку задачи я в самом начале описал, и проблема вполне реальная. Я уже измерял когда-то, каждая блокировка на мьютексе занимает 40 микросекунд. Вот в тех нечастых случаях когда это время критично и требуются неблокирующие алгоритмы.


  1. Antervis
    22.11.2016 10:01
    +2

    Майерс в «Эффективном и современном с++» упоминал, что операции со счетчиками атомарны.
    Вот вы исследовали реализацию gcc, а что по поводу потокобезопасности разделенного указателя говорит стандарт?


  1. iperov
    22.11.2016 11:55
    -9

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


    1. mayorovp
      22.11.2016 12:33

      И часто вы их расширяете? :)


    1. semenyakinVS
      22.11.2016 14:06
      +3

      Я сам когда-то таким занимался — делал свои умные указатели с poly-based дизайном (для настройки всяких вещей вроде политик владения, многопоточности, источников привязываемых объектов, и т.д.). Даже статью на хабру писал по теме… Но понял в какой-то момент, что мои реализации работают намного хуже стандартных. Выбросил свои реализации, превратив уже сделанные классы в template-врапперы над классами из stl. Работа не совсем напрасная — теперь я могу подменять реализации, если случится чудо и кто-то сделает лучше чем stl — но неприятный осадок остался.

      Если ваши указатели действительно лучше стандартных указателей — напишите статью, интересно будет почитать. Только не забудьте привести доказательства преимуществ вашего решения по каким-нибудь метрикам (быстродействие, расход памяти, удобство использования, и т.д.).


      1. iperov
        22.11.2016 14:36
        -5

        это ненужная борьба, все кому не нравится C++ давно сделали свои ЯП


    1. fgengine
      22.11.2016 15:40

      А может напишите статью о вашей реализации или поделитесь исходниками?


    1. vidyacat
      22.11.2016 20:49
      +6

      на словах ты дональд кнут…


      1. hdfan2
        23.11.2016 07:45
        +1

        … а на деле — простой пряник.


  1. Readme
    22.11.2016 18:14

    Тема неблокирующих контейнеров и алгоритмов (абстракций уровня более высокого, чем std::atomic<> и их интерефейса) сложная и интересная, более того, в настоящий момент ведутся активные исследования на эту тему (пишутся диссертации (!), берутся патенты и т.д.). Настоятельно рекомендую автору познакомиться с замечательной книгой C++ Concurrency in Action — Practical Multithreading Э. Уильямса (в русском переводе: Параллельное программирование на С++ в действии. Практика разработки многопоточных программ). Главы 4-7 весьма сложны для понимания, но в них рассматривается модель памяти нового стандарта и некоторые рецепты, как с её помощью и с новыми примитивами можно строить lock-free структуры. Также в книге детально разобрано поэтапное построение lock-free очереди, из которого можно извлечь весьма ценные уроки работы с CAS и вообще atomic'ами (как, например, одновременное измененние структуры наподобие указатель-счётчик (или как без этого можно обойтись), что, уверен, автору будет особенно интересно).


    1. degs
      22.11.2016 18:21

      Спасибо за настоятельную рекомендацию, только что вас заставляет думать что я ее не читал?
      И, да, мне действительно было интересно и полезно, но уже довольно давно, поскольку книга вышла в 2012


      1. Readme
        22.11.2016 18:53

        Меня смутила некоторая категоричность выводов статьи. Действительно, если пользователи каким бы то ни было образом получили доступ к raw-указателю (под капотом ли он shared_ptr или нет) — да, никакая сила не спасёт многопоточную среду от гонки. Но, например, в озвученной книге довольно часто обращалось внимание на тот факт, что для достижения lock-free зачастую приходится чем-то жертвовать, что в реализации выливалось в использование proxy-классов возврата или идиомы «создать-и-обменять» (что близко к понятию транзакций). Если абстрагироваться от святого желания сделать shared_ptr таким же быстрым, как и raw-указатели, думаю, с использованием сложных proxy и, возможно, в будущем transactional memory (если появится и войдёт в стандарт) или её эмуляцией в текущих реалиях стандарта, реализовать lock_free shared_ptr всё-таки возможно.


        1. Readme
          22.11.2016 19:01

          … хотя тут надо не запутаться в пресловутом lock-free — что и где мы считаем свободным от блокировок. А то может сложиться впечатление, что простая обёртка в такой atomic_shared_ptr любого контейнера сделает его и весь его интерефейс lock-free.


        1. degs
          22.11.2016 19:06

          Ну наверное вы в чем-то правы, но вообще пост не о lock-free — просто попытка разобраться из любопытства что именно мешает std::shared_ptr<> быть thread safe. Тема неблокирующих алгоритмов возникает фактически только потому что атомарные операции там уже присутствуют при работе со счетчиком.


          1. Readme
            22.11.2016 19:20

            Да, прошу прощения, унесло немного не туда :)
            К вопросу о thread-safe текущей реализации — думаю, в свете упомянутого, цена такого решения в виде громоподобно хрустящих compare_exchange под капотом не совсем соотносится с возможным выигрышем от всей этой кухни, поэтому комитету (и пользователям) было и будет проще пока что довольствоваться блокирующим доступом и специализациями std::atomic_xxx(shared_ptr<...>) или собственноручно добавленным мьютексом/спинлоком, благо в стандарте чётко оговаривается этот момент (мол, «шарьте, но не один указатель»).


            1. degs
              22.11.2016 20:17

              compare_swap был и остается единственным условным атомарным оператором, без него вам ни одого нетривиального алгоритма не создать. Да, он сразу же переводит алгоритм в класс waiting, но как правило это не фатально, те же виртуальные функции вовсю используются в реализации.
              Вообще, я препочитаю стандарты не обсуждать а понимать. В часности при создании std::shared_ptr был выбран очевидный баланс между тяжеловесностью реализации и универсальностью.


  1. degs
    22.11.2016 18:20

    ошибся веткой


  1. tgz
    22.11.2016 22:11
    +1

    Как хорошо что есть rust…


    1. degs
      22.11.2016 22:41
      +1

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


      1. tgz
        23.11.2016 08:54

        Ну как минимум через пару лет там не появится еще один atomic_uniq_shared_locked_fast_super_auto_ptr.


        1. DarkEld3r
          23.11.2016 12:56

          Правильно я понимаю, что проблема только в названии? Какой-нибудь Arc<Mutex<Box<_>>> будет (сильно) лучше?


          Если что мне весьма нравится раст, но именно эта претензия не понятна.


          1. tgz
            24.11.2016 11:23

            Конечно сильно. Arc, Mutex и Box я могу использовать (или не использовать) в любом порядке с любой вложенностью. Ну и не надо ждать 15 лет выхода очередного стандарта что бы что-то новое появилось в компиляторе.
            Ничего не поделать, legacy всегда начинает убивать монстров. И это правильно.


            1. DarkEld3r
              24.11.2016 12:47
              +2

              Arc, Mutex и Box я могу использовать (или не использовать) в любом порядке с любой вложенностью.

              Это удобно, не спорю.


              Ну и не надо ждать 15 лет выхода очередного стандарта что бы что-то новое появилось в компиляторе.

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


        1. semenyakinVS
          23.11.2016 14:19
          -1

          Вот как раз чтобы избежать такого, в плюсах есть традиция использовать policy-based design и typedef для стандартных специализаций подобных шаблонов (ну, и ещё auto для ленивых — хотя я лично отношусь к нему отвращением).


        1. degs
          23.11.2016 15:29

          М-да? А если будет придуман новый алгоритм, лучший во всех отношениях, и в расте он не появится? Это хорошо или наоборот плохо?


          1. Antervis
            24.11.2016 05:50

            для яп еще никто (и хорошо) не вводит копирайты на алгоритмы. Появится наилучшая реализация из возможных? — она будет использоваться везде


  1. Door
    23.11.2016 00:21

    Да, нужно использовать atomic-операции для свапа: C++ 11 std::atomic_...<std::shared_ptr>


  1. mayorovp
    23.11.2016 11:15

    Вот так комбинация атомарного инкремента и атомарного декремента приводит к гонке между потоками и std::shared_ptr<> не является потокобезопасным, даже на уровне контрольного блока. Вот теперь действительно точка.

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


    1. degs
      23.11.2016 15:26

      Согласен, я в общем и пытался продемонстрировать что имеется ввиду в этом утверждении.
      Но, кстати, ваша формулировка тоже неоднозначна, что значит "могут безопасно указывать"?


      1. mayorovp
        23.11.2016 15:34

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


        1. encyclopedist
          24.11.2016 02:59

          возможно тогда "разделяемых"?


          1. mayorovp
            24.11.2016 06:06

            Как раз для разделяемых указателей ничего не гарантируется, в посте это хорошо показано.


  1. abby
    24.11.2016 14:35
    -1

    Как ответили выше, это в общем-то и в документации есть. То, что Вы хотите можно сделать при помощи std::weak_ptr. std::weak_ptr::lock должна быть атомарна.
    Псевдо-код:

    int main() {
      uint32_t i = 10000;
      while (i-- > 0) {
        auto sp = std::make_shared<int>();
        std::weak_ptr p = sp;
        thread([p]{
          if (auto sp = p.lock()) {
            // pointer is valid, Cool, use sp
          } else {
            // pointer is already expired, ignore
          }
        }).detach();
      }
      return 0;
    }
    


    1. mayorovp
      24.11.2016 15:02

      Не вижу у вас обновления разделяемого между потоками указателя.


      1. abby
        24.11.2016 16:40

        Исходный sp удаляется сразу после std::thread::detach, потому что «заканчивается scope», при этом в другом потоке мы пытаемся получить к нему доступ через std::weak_ptr.


        1. mayorovp
          24.11.2016 16:47

          И что дальше? Какое отношение это имеет к ситуации, разобранной автором поста?


          1. abby
            24.11.2016 17:48

            промахнулся, ответ ниже.


  1. abby
    24.11.2016 17:46

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

    Однако же я, прослышав что std::shared_ptr дает потокобезопасный доступ к контрольному блоку (и не очень понимая что это значит), а так же что операции над ним реализованы lock-free, хочу предложить свое изящное решение:

    далее идет пример, того как делать не надо, а, как сказали выше, надо было просто дальше прочитать документацию. Хорошо, но как решить задачу без дополнительных финтов с блокировками?

    Заметим, что читающий поток может пропустить часть данных, тогда это можно сделать, используя weak_ptr (считаем, что писатели могут синхронизироваться отдельно, тут задержка не важна). При этом, я считаю, что существует неблокирующий способ передачи этого самого weak_ptr в читающий поток.

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


    1. mayorovp
      24.11.2016 18:59

      Вы не показали неблокирующего способа передачи изменяемого weak_ptr в поток.


    1. degs
      24.11.2016 19:21

      Естественно я рассматривал случай weak_ptr, в пост добавлять не стал чтобы не загромождать ненужными деталями. Валится точно так же, и механизм тот же, даже lock() не требуется. Вообще-то могли бы и проверить перед тем как постить.


      void read_data() {
          for(;;)
              std::weak_ptr<int> sp=data;
      }
      
      int main()
      {
          std::thread(read_data).detach();
          for(;;)
              data=std::make_shared<int>(0);
          return 0;
      }


      1. abby
        24.11.2016 21:28

        Тут та же самая ошибка, если один из потоков использует неконстантные методы доступа, то будет data race.

        All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object. If multiple threads of execution access the same shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.
        http://en.cppreference.com/w/cpp/memory/shared_ptr

        Как передать в другой тред? — использовать другие неблокирующие структуры данных, например, попробовать неблокирующие очереди, но это пахнет тем же мьютексом на спин-локах. Хотя производительность надо мерить в конкретном случае.

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

        На самом деле я был бы рад ошибиться, возможно есть что-то, что я не знаю или понимаю неправильно?
        А не посмотрите реализацию std::experimental::atomic_shared_ptr?


        1. degs
          24.11.2016 22:26

          А не посмотрите реализацию std::experimental::atomic_shared_ptr?

          Ну, если то что я вижу это апдейтнутая версия, то там ничего нового не добавлено, просто враппер вокруг стандартных std::atomic_… свободных функций. Пока больше похоже на заготовку на будущее.


          1. abby
            25.11.2016 01:26
            +1

            Вот тут, похоже, интересные мысли с использованием Differential Reference Counting.
            boostcon/cppnow 2016/implementing_a_lock_free_atomic_shared_ptr.pdf
            И там дальше по ссылке литературы www.1024cores.net differential-reference-counting


            1. degs
              25.11.2016 03:43

              Ссылки интерсные, спасибо. Отпишусь когда прочитаю.


            1. degs
              26.11.2016 05:37

              Да, все верно. Но понимаете, это новые алгоритмы основанные на 128-битных атомиках, которые как бы уже есть но как бы еще не везде, поэтому до вхождения в стандарт им пока далеко. Я этой темы специально не касался, она конечно безумно интересная, но о ней нужно писать отдельно.