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

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

Это может быть сделано несколькими способами, но мы будем говорить только о двух из них: хэндлы (handles) и встроенные счётчики ссылок (intrusive reference counters).

Хэндлы — это небольшие структуры, которые содержат указатель на объект данных и вспомогательные данные, чтобы гарантировать, что объект еще жив. Как правило для работы с хэндлами пишутся две функции: lock_handle и unlock_handle (имена выбраны произвольно, чтобы показать функциональность). Lock_handle проверяет «живость» объекта, увеличивает атомарный счетчик ссылок и возвращает указатель на объект данных, если он ещё доступен. Если нет, то функция возвращает NULL, или с помощью другого способа даёт знать, что объект больше не доступен. Соответственно своему названию, unlock_handle атомарно уменьшает счетчик ссылок и как только он достигает значения 0, удаляет объект.

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

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

Встроенные счетчики ссылок

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

struct data_packet_s {
  void *data_buffer;
  size_t data_buffer_size;
};

=>

struct data_packet_s {
  void *data_buffer;
  size_t data_buffer_size;
  volatile int reference_count;
};

Именно это является главным недостатком этого подхода: он нуждается в модификации структуры данных, поэтому использовать его можно только для таких структур, которые мы можем изменить. Мы не можем использовать его с произвольным типом данных или структурами, например, библиотечными.

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

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

image

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

image

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

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

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

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

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

Итак, давайте просуммируем недостатки и преимущества встроенных счетчиков ссылок:

Недостатки:
  • Встроенные счётчики требуют изменения структур данных
  • Ссылка должна оставаться залоченной пока есть вероятность что объект будет использован
  • Собственность ссылок передаётся между потоками
  • Необходимо внимательное отношение к косвенно переданным объектам

Преимущества:
  • Не требуются дополнительные структуры и операции с памятью

Хэндлы

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

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

struct handle_s {
  volatile int *reference_count;
  void *object;
};

Где reference_count выделяется при создании первого хэндла на данный объект.

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

Типичное применение такого хэндла будет следующим:

struct some_struct_s *object = lock_handle(hdata);

if(object) {
  use(object);
  release_handle(hdata);
};

Давайте посмотрим, что происходит, когда последняя ссылка на объект удалена. Прежде всего, мы, очевидно, хотим, чтобы удалился управляемый объект. Если объект является простым куском выделенной памяти, мы просто хотим, чтобы память, используемая объектом была освобождена. Но в большинстве случаев это не так. Как и в вышеупомянутом примере data_packet_s, где мы хотели бы освободить также и память data_buffer. Если мы используем хэндл для только одного типа объектов, это не создаёт большой проблемы. Но если мы хотим, чтобы хэндл мог обрабатывать разыличные типы, это привносит ещё один вопрос: как правильно удалить управляемый объект?
Мы можем добавить в хэндл ещё одно поле: указатель на функцию, которая будет использоваться для уничтожения/освобождения управляемого объекта. Теперь хэндл выглядит следующим образом:

struct handle_s {
  volatile int *reference_count;
  void *object;
  void (*destroy_object)(void*);
};

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

После этого мы хотели бы освободить память, которая содержит reference_count. Но не тут то было: сделать это будет ужасной ошибкой. Если другие хэндлы на этот же объект до сих пор хранятся в других потоках, после освобождения памяти они будут ссылаться на reference_count, который уже удален. А на следующей попытке получить доступ к объекту, мы будем иметь попытку обращения к освобожденной памяти.

Есть ли решение, которое позволило бы не допустить утечки памяти но при этом избежать обращений к освобождённой памяти счетчиков ссылок? Такой способ есть. Это пул объектов, который будут управлять освобожденными счетчиками. И вот тут-то и появляется проблема, найти которую может быть довольно непросто, и которая известна как «проблема ABA». Представьте себе, ситуацию, когда у вас есть хэндл на объект. Один из потоков удаляет объект. Затем конструируется другой объект, и для него создается управляющих хэндл. Что при этом произойдет?

Когда объект уничтожен, reference_count связанный с данным объектом (назовем его object1) освобождается обратно в пул объектов со значением 0. Пока что всё идёт по плану. Но когда выделяется другой хэндл для нового объекта (назовем его object2), то reference_count, который будет связан с этим объектом берется из пула объектов, при этом данный reference_count устанавливается в 1. Теперь представьте, что поток хранящий хэндл на object1 пытается получить указатель. Это удастся, потому что reference_count на который указывает данный хэндл уже не 0, хотя он и принадлежит теперь object2. Функция блокировки вернет неверный указатель, программа (если повезёт) потерпит крушение, или (если не повезёт) повредит содержимое памяти обращением к освобожденному участку.
Решение, разумеется, существует, иначе я бы не писал всё это.

Мы хотим сделать структуру handle_s настолько легкой, насколько это возможно, чтобы иметь возможность передать ее по значению, а не по указателю, так что сделаем следующее: создадим две структуры, одна из которых будет «слабым» хэндлом, то есть не ограничивать и не проверять объект, которым она управляет, а другой будет «сильным» хэндлом, т.е. таким, который будет иметь жесткую связь с конкретным управляемым объектом и вернет NULL, если «слабый» хэндл связанный с ним больше не ссылается на тот же объект.

Давайте определим их так:
struct weak_handle_s {
  volatile int version;
  volatile int reference_count;
  void object;
  void (*destroy_object)(void*);
};

struct strong_handle_s {
  struct weak_handle_s *handle;
  int version;
};

Итак, как вы видите, теперь обе ручки имеют поле «version», и strong_handle_s уже не имеет указателя на объект, так как он теперь хранится в общем для объекта weak_handle_s.

Давайте посмотрим, как он защищает нас от проблемы ABA, показанной выше.

В strong_handle_s и weak_handle_s, которые ссылаются на один и тот же объект имеются поля «version», которые равны друг другу.
Всякий раз, когда ручка освобождается и weak_handle_s помещается обратно в пул объектов, мы будем увеличивать номер версии в weak_handle_s.

В следующий раз, если освобожденный в пул weak_handle_s переиспользуется для обработки другого объекта, он будет иметь номер версии, отличный от номера версии который был у объекта, который был освобожден. Теперь в функции lock_handle, сравнивая поля версии в обоих, «слабом» и «сильном» хэндле, мы можем сказать, ссылается ли weak_handle_s по-прежнему на тот объект, указатель которого мы пытаемся получить из strong_handle_s, и вернуть NULL, если это не так.

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

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

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

Недостатки:
  • Нуждается в дополнительной структуре и памяти.
  • Занимает дополнительное время процессора
  • Добавляет некоторые неочевидные проблемы

Преимущества:
  • Позволяет сохранять хэндлы и иметь к ним доступ по мере необходимости.
  • Не требует того, чтобы объект постоянно находился в собственности.

Выводы

Встроенный счетчик ссылок реализует общие сильные ссылки (strong reference), которые должны удерживать счетчик ссылок, пока объект не будет гарантированно невостребован. Пока все ссылки не будут разлочены, объект не будет удален. Если ссылка разблокирована в потоке, который имел в собственности лишь одну ссылку, этот поток уже больше не сможет гарантированно безопасно получить еще одну ссылку.

Хэндл реализует общую слабую ссылку (weak reference). Если объект жив, функция lock_handle вернет указатель на запрашиваемый объект, и объект гарантированно не будет удален, пока хэндл не будет разлочен. Это безопасно для блокировки и разблокировки сколько угодно раз, так как хэндл является отдельным объектом, и гарантированно является валидным участком памяти. Соответствующие меры должны быть приняты, чтобы гарантировать, что разделяемая память счетчика ссылок не освобождается, когда объект достигает счетчика ссылок 0, и что повторно используемый счетчик ссылок не используется, чтобы проверить количество ссылок на уже освобожденные объекты.

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


  1. kmu1990
    15.09.2015 14:57
    +3

    А по какому принципу вы расставляете volatile? Почему, например, у поля version структуры weak_handle_s его нет? Ну и вообще зачем тут volatile, ведь как я понял из вступления, счетчик ссылок предполагается атомарным, volatile ведь тут ничем не помощник, разве нет?


    1. vladvic
      15.09.2015 15:08

      Да, счётчик ссылок предполагается атомарным, и по сути он volatile. Да, Вы правы, в gcc атомарный доступ будет работать правильно при любой оптимизации и без volatile, но я не готов отвечать за другие компиляторы. Про version в weak_handle_s — это тоже верно заметили: если на совесть, там тоже должен стоять volatile.


  1. lorc
    15.09.2015 16:02
    +1

    Не совсем понятно при чем тут потоки, если описанные проблемы точно так же существуют в однопоточной среде.
    Потом, непонятно, почему

    «Ссылка должна оставаться залоченной пока есть вероятность что объект будет использован»

    является минусом. Это нормальное поведение strong reference. И это совершенно нормально, когда ссылка должна оставаться пока есть вероятность что объект будет использован. Вы же на него ссылаетесь? Значит счетчик ссылок не 0. Если вам объект больше никогда не нужен, вы делаете
    unref(ptr);
    ptr = NULL;
    

    Что бы исключить любые попытки использовать объект после освобождения. И всё.

    Кстати, ваш handle не решает проблемы передачи объекта между потоками.


    1. vladvic
      15.09.2015 17:49

      В однопоточной среде описанные проблемы решаются гораздо проще, а ещё в однопоточной среде гораздо проще такие проблемы не создавать в принципе.
      В данном случае рассматривается применение именно в многопоточной среде, потому что именно в многопоточной среде проблема одновременного общего доступа к данным наиболее актуальна.
      Хэндл решает проблему передачи объекта между потоками в том плане, что передав в поток хэндл мы всегда можем попробовать залочить объект и убедиться в том что он жив, или же понять, что объект уже умер не создавая опасных ситуаций (см. пример 1 с обращением к освобожденной памяти).
      Согласен, это нормальное поведение strong reference, поэтому это минус не конкретного подхода, а в принципе техники сильных ссылок. Пожалуй я неправильно сформулировал в чём заключается минус. Минус в том что мы не можем произвольно освобождать объект на время а потом пытаться залочить вновь, если он временно не требуется.


      1. lorc
        15.09.2015 18:25

        В данном случае нет никакой разницы между однопоточной и многопоточной средой. Собственно проблема совместного владения объектами (а вы решаете именно её) довольно таки ортогональна многопоточности. По большему счету, для поддержки многопоточности надо просто решить проблему атомарности счётчика ссылок.

        потому что именно в многопоточной среде проблема одновременного общего доступа к данным наиболее актуальна.

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

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

        Вам надо или залочить его в первом треде или с вероятностью 50% вы получите null во втором треде. Конечно, опасной ситуации это не создаст, но и объекта вы не получите. Толку от такой надежности?
        Минус в том что мы не можем произвольно освобождать объект на время а потом пытаться залочить вновь, если он временно не требуется.

        Хм, а как это «временно не требуется»? Он вам либо нужен и вы держите его залоченым (это же не мютекс который нужно отпустить как можно раньше), либо он вам больше совсем не нужен и вы его отпускаете навсегда.


        1. vladvic
          15.09.2015 19:44

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

          Я с удовольствием прочитаю если вы расскажете как решить описанную мной проблему с помощью примитивов синхронизации.
          Вам надо или залочить его в первом треде или с вероятностью 50% вы получите null во втором треде. Конечно, опасной ситуации это не создаст, но и объекта вы не получите. Толку от такой надежности?

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

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


          1. lorc
            15.09.2015 20:14
            +1

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

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

            Тем не менее в этой ситуации у handle нет никаких преимуществ перед обычным встроенным счётчиком ссылок. И там и там надо делать ref() в вызывающем потоке. Поэтому я не понимаю, зачем вы приводили этот пример.

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

            Ну смотрите. Если три варианта:
            1) Вам нужен сокет (прямо сейчас или в будущем, не важно). Вы держите на него ссылку.
            2) Сокет вам больше не нужен. Вы удаляет ссылку и забываете о нём насовсем.
            3) Было бы неплохо иметь сокет, но он не обязателен. Это вариант weak refference. Вы пытаетесь взять ссылку тогда, когда он вам нужен, но вас не расстроит если сокет уже удален.

            Вы предлагаете использовать ваш handle именно в режиме weak refference, т.е. отпускать ссылку сразу после использования и потом молиться, что кто-то ещё держит этот сокет, что бы вы могли взять ссылку ещё раз. Но обычно вы будете единственным владельцем сокета, и как только вы сделаете unref() — сокет будет удален. Да, потом вы сделаете ref() и получите null. У вас ничего не сломается, но и работать ничего не будет. Это нормальное поведение для weak ptr. Но на самом деле слабые ссылки нужны очень редко.
            Гораздо чаще нужны именно strong ptr. И ваш handle тоже можно использовать в таком режиме. Но вы почему-то делаете упор на weak ptr и предлагаете использовать ваше handle именно так.


            1. vladvic
              15.09.2015 20:24

              Тем не менее в этой ситуации у handle нет никаких преимуществ перед обычным встроенным счётчиком ссылок. И там и там надо делать ref() в вызывающем потоке. Поэтому я не понимаю, зачем вы приводили этот пример.

              В данной ситуации (в первой, там где ошибка) у handle есть одно огромное преимущество: он не вызовет ошибок обращения к памяти.
              Потому что вы путаете понятие общего доступа и общего владения.

              Общее владение это общее владение. Общий доступ — это общий доступ. В данном случае рассматривается общий доступ к памяти. Общее владение тут указано как один из способов гарантировать сохранность выделенного участка памяти.
              Вы предлагаете использовать ваш handle именно в режиме weak refference, т.е. отпускать ссылку сразу после использования и потом молиться, что кто-то ещё держит этот сокет, что бы вы могли взять ссылку ещё раз.

              Я предлагаю использовать хэндл именно в режиме weak reference, т.е. ссылки на объект, а не владение объектом. В моей практике есть десятки случаев когда нужен именно weak reference.
              Например, предложите мне другой способ избежать циркулярных зависимостей.


              1. lorc
                15.09.2015 20:37

                В данной ситуации (в первой, там где ошибка) у handle есть одно огромное преимущество: он не вызовет ошибок обращения к памяти.

                Правильное использование handle не вызовет ошибки обращения к памяти.
                Правильное использование тупого счетчика ссылок тоже не вызовет ошибок обращения к памяти.
                Неправильное использование refcount(сделали get() после put()) вызовет ошибки обращения к памяти.
                Неправильное использование handle (забыли проверить на null) вызовет ошибки обращения к памяти.
                В чем разница?
                В данном случае рассматривается общий доступ к памяти

                Извините, но в данном случае не рассматривается общий доступ к памяти.
                Что такое по вашему общее владение?
                И что такое общий доступ к памяти?
                В моей практике есть десятки случаев когда нужен именно weak reference.

                Но случаев когда нужна именно strong refference всё же больше, разве нет?
                Например, предложите мне другой способ избежать циркулярных зависимостей.

                Ну да, циклические зависимости — это как раз то место где и нужен weak ptr. Общий на всю систему сокет — это не то место, где нужен weak ptr.


                1. vladvic
                  15.09.2015 20:44

                  Ну да, циклические зависимости — это как раз то место где и нужен weak ptr. Общий на всю систему сокет — это не то место, где нужен weak ptr.

                  А сокет-то тут причём? Вы что, сокет решили счётчиками считать?
                  Я говорил о других объектах с которыми может идти манипуляция после вычитывания данных.


                1. vladvic
                  15.09.2015 20:53

                  Правильное использование handle не вызовет ошибки обращения к памяти.
                  Правильное использование тупого счетчика ссылок тоже не вызовет ошибок обращения к памяти.

                  Ну вот, мы уже почти там.
                  Handle невозможно использовать по назначению так чтобы вызвать ошибку обращения к памяти. Если вы забыли проверить на NULL в бэктрейсе дампа вы это сразу увидите.
                  С тупым счетчиком ссылок это всё же возможно. При этом ошибку вы можете увидеть далеко не сразу при обращении к памяти, а гораздо, гораздо позже.
                  Извините, но в данном случае не рассматривается общий доступ к памяти.

                  Гарантия безошибочности чтения и записи в выделенный кусок памяти — это не доступ?
                  Проблема ABA с хэндлами — это не доступ?
                  Давайте не заниматься придирками и безудержным критиканством.
                  Но случаев когда нужна именно strong refference всё же больше, разве нет?

                  Как ни странно, случаев когда нужны именно strong reference у меня как раз меньше. Как правило один объект или трэд порождает объект, он им и владеет и занимается. А другие потоки им только пользуются по мере надобности.


                  1. lorc
                    15.09.2015 21:10
                    +1

                    Если вы забыли проверить на NULL в бэктрейсе дампа вы это сразу увидите.

                    Т.е. разница только в том, что я получу гарантированный бэктрейс? Окей.
                    Гарантия безошибочности чтения и записи в выделенный кусок памяти — это не доступ?

                    Я под безошибочным чтением подразумеваю то, что я увижу консистентное состояние объекта. Это обеспечивается или синхронизацией потоков или lock-free алгоритмами. То же самое с записью. Это да, это доступ.
                    А то что мы не словим segfault при чтении байта по определенному адресу — это… ну даже не знаю что это.
                    Проблема ABA с хэндлами — это не доступ?

                    Проблема ABA тут возникает из-за использования пула, который вы ввели для организации weak refference. И да, это не доступ, это проблема.
                    Давайте не заниматься придирками и безудержным критиканством.

                    Это не придирки и не критиканаство. Вы молодец что написали статью, но ваша постановка задачи неверна. И поэтому вы сделали неверные выводы в конце статьи. Я всего лишь пытаюсь донести это до вас.

                    Как правило один объект или трэд порождает объект, он им и владеет и занимается.

                    Ну хорошо. Просто мой опыт (я в основном ковыряюсь в ядре linux) говорит совершенно об обратном.


                    1. vladvic
                      15.09.2015 21:38

                      это… ну даже не знаю что это.

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

                      А что конкретно вам не понравилось в постановке задачи? Я от вас из критики услышал только то, что «управление циклом жизни объекта ортогонально многопоточности».
                      И что показалось неверным в выводах, которые, я считаю, довольно очевидны? Ну кроме, возможно, упомянутого вами минуса, который действительно как минус довольно спорен?


                      1. lorc
                        15.09.2015 23:03
                        +1

                        Ну вот это как раз одно из важнейших условий консистентности объекта.

                        Ну в некотором смысле да. Но это всё зависит от среды. Например в java или .net в принципе не может возникнуть этой проблемы. Если у нас есть ссылка на объект, значит объект живой (да, я знаю про Dispose() ), но тем не менее проблемы общего доступа к памяти там встают в полный рост. Поэтому как то принято отделять управление жизненным циклом объекта от собственно управления общим доступом к объекту. Эти вещи местами конечно же пересекаются (не может быть общего доступа, если объект больше/еще не существует), но тем не менее — это разные концепции.

                        А что конкретно вам не понравилось в постановке задачи?

                        Некоторый упор на многопоточность. Так же, то что вы записали в минусы подсчета ссылок «Ссылка должна оставаться залоченной пока есть вероятность что объект будет использован» хотя это не минус, а нормальное явление. Никаких лишних ресурсов это не тратит, ни к чему плохому привести не может (это же не мютекс в залоченый).
                        И что показалось неверным в выводах, которые, я считаю, довольно очевидны?

                        Опять же упор на многопоточность. И лишнее напоминание о том как плохо держать залоченую ссылку: «Если ссылка разблокирована в потоке, который имел в собственности лишь одну ссылку, этот поток уже больше не сможет гарантированно безопасно получить еще одну ссылку.». Это очевидно, как по мне и не может являеться минусом.

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

                        Знаете, если бы вы назвали статью «Реализация сильных и слабых ссылок в C», у меня не было бы совершенно никаких вопросов. Ну кроме одного :) Как вы планируете бороться с переполнением версии?


                        1. niamster
                          16.09.2015 02:27

                          Подобная проблема возникает при циклических зависимостях.

                          В C++11 для этого есть std::weak_ptr. В сети полно статей о том как это работает.
                          Если я правильно понял реализацию, то weak_ptr и shared_ptr используют(разделяют) специальный объект, который управляет 2мя счетчиками — weak и strong. Оригинальный обьект удаляется когда strong_count == 0, а сам объект счетчика уничтожается когда weak_count == 0. Главная идея в том, что сам shared_ptr понимает о существовании weak_ptr. При разыменовании weak_ptr проверяется значение strong_count и если оно равно 0 — попытка завершается неудачей.


                        1. vladvic
                          16.09.2015 04:47

                          Ну в некотором смысле да. Но это всё зависит от среды. Например в java или .net в принципе не может возникнуть этой проблемы.

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

                          Я даже ОПИСАЛ, какую. И не только хэндлы, а и интрузивные указатели тоже.
                          Как вы планируете бороться с переполнением версии?

                          Я не планирую с ней бороться. Практически при переполнении версии случай коллизии практически невероятен, если вы конечно не используете под неё unsigned char. Всё что больше этого даёт достаточно четкие гарантии.
                          Но поднятый вопрос понят, и, что называется acknowledged.


        1. vladvic
          15.09.2015 19:57

          В данном случае нет никакой разницы между однопоточной и многопоточной средой. Собственно проблема совместного владения объектами (а вы решаете именно её) довольно таки ортогональна многопоточности. По большему счету, для поддержки многопоточности надо просто решить проблему атомарности счётчика ссылок.

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


          1. lorc
            15.09.2015 20:25

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


            1. vladvic
              15.09.2015 20:42

              Просто как я говорил понятие владения довольно ортогонально к многопоточности.

              Давайте обойдемся без пустого критиканства, хорошо? Понятие «алгоритм» тоже «ортогонально» к понятию «программирование».
              кроме «атомарного» volatile и одного натянутого примера

              И без передёргивания понятий, если можно. volatile — это спецификация переменной, а «атомарный» — доступ к ним. В любом приложении хорошим тоном является обозначение с помощью volatile тех переменных которые изменяются более чем в одном потоке, и изменения которых важны для состояния.
              на самом деле. Вам ещё нужен мютекс на пул

              Возможно вы заметили, что вопроса организации собственно пула я не касался. И думаю что вам так же как мне известно, что давно уже существует порядочное количество lock-free алгоритмов для почти любых контейнеров.
              Что в статье есть такого (кроме «атомарного» volatile и одного натянутого примера), что имеет какое-либо отношение к многопоточности?

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


              1. lorc
                15.09.2015 20:56

                В однопоточном приложении счётчика ссылок более чем достаточно для определения времени жизни объекта. И хэндлы в однопоточном приложении не нужны.

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


                1. vladvic
                  15.09.2015 21:01

                  Мммм, какое преимущество хэндлы имеют именно в многопоточных приложениях? Я прочитал вашу статью, комментарии, но так и не понял.

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

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


                  1. lorc
                    15.09.2015 21:11

                    Хорошо. Итак, какое преимущество хэндлы имеют именно в многопоточных приложениях?


                    1. vladvic
                      15.09.2015 21:17

                      Давайте поставим вопрос по иному. Я не вижу никакого смысла использовать хэндлы в приложениях с одним потоком.


                      1. lorc
                        15.09.2015 21:26

                        Забавно. А я вижу :)
                        Ваши хендлы действительно полезная штука. Полезная потому, что они реализуют механизм weak reference, который одинаково нужен, что в многопоточных, что однопоточных приложениях. Например для разрыва циклических ссылок ;)
                        В этом и есть их преимущество перед обычным reference count.

                        А разговоры о многопоточности — тут совершенно в кассу, извините.