Счетчик ссылок без блокировок и интеллектуальные указатели C ++

В общем, наиболее широко известные реализации классов smart ptr с подсчетом ссылок в C ++, включая стандартный std::shared_ptr, используют атомарный подсчет ссылок, но не предоставляют атомарный доступ к одному и тому же экземпляру smart ptr. Другими словами, несколько потоков могут безопасно работать с отдельными shared_ptr экземплярами, которые указывают на один и тот же общий объект, но несколько потоков не могут безопасно читать / записывать экземпляры одного и того же shared_ptr экземпляра без обеспечения какой-либо синхронизации, например мьютекс или что-то еще.

Атомарная версия shared_ptr под названием "atomic_shared_ptr" была предложена и предварительные

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

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

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

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

Учитывая всю эту сложность, я был очень удивлен, обнаружив статью доктора Доббса не ранее 2004 (задолго до атомики C ++ 11), которая, кажется, небрежно решает всю эту проблему:

http://www.drdobbs.com/atomic-reference-counting-pointers/184401888

Похоже, автор утверждает, что каким-то образом может:

«... [читать] указатель на счетчик, увеличивает счетчик и возвращает указатель - все таким образом, что никакие другие потоки не могут вызвать неправильный результат»

Но я не совсем понимаю, как он это на самом деле реализует. Он использует (непереносимые) инструкции PowerPC (примитивы LL / SC lwarx и stwcx), чтобы осуществить это.

Соответствующий код, который делает это, он называет "aIandF" (атомарное приращение и выборка), который он определяет как:

addr aIandF(addr r1){
  addr tmp;int c;
  do{
    do{
      tmp = *r1;
      if(!tmp)break;
      c = lwarx(tmp);
    }while(tmp != *r1);
  }while(tmp && !stwcx(tmp,c+1));
  return tmp;
};

Очевидно, addr - это тип указателя, указывающий на общий объект, которому принадлежит переменная счетчика ссылок.

Мой вопрос (ы):: возможно ли это только с архитектурой, которая поддерживает операции LL / SC? Кажется, с cmpxchg не обойтись. А во-вторых, как именно это работает? Я прочитал этот код несколько раз и не могу понять, что происходит. Я понимаю, что делают примитивы LL / SC, просто могу " Я не разбираюсь в коде.

Лучшее, что я могу понять, это то, что addr r1 - это адрес указателя на общий объект, а также также адрес указателя на счетчик ссылок (что, как я полагаю, означает, что переменная счетчика ссылок должна быть первый член struct, определяющий общий объект). Затем он разыменовывает addr (получая фактический адрес общего объекта). Затем он связал загружает значение, хранящееся по адресу в tmp, и сохраняет результат в c. Это значение счетчика. Затем он условно сохраняет это увеличенное значение (которое не удастся, если tmp изменилось) обратно в tmp.

Я не понимаю, как это работает. Адрес совместно используемого объекта может никогда не измениться, и LL / SC может быть успешным - но как это нам поможет, если другой поток тем временем освободил разделяемый объект?


person Siler    schedule 17.05.2016    source источник
comment
@Anthony Williams: Не могли бы вы прокомментировать это как автор одной из реализаций atomic_shared_ptr?   -  person Kerrek SB    schedule 18.05.2016
comment
Релевантно: stackoverflow.com/questions/1147904/   -  person Barry    schedule 18.05.2016
comment
aIandF кажется эквивалентом встроенного атомарного GCC _2 _ / _ 3_.   -  person jxh    schedule 18.05.2016
comment
@jxh Но это блокируется.   -  person Barry    schedule 18.05.2016
comment
@Barry: stackoverflow.com/questions/27837731/is-x86-cmpxchg -атомный   -  person jxh    schedule 18.05.2016
comment
Один из подходов - спроектировать потоки так, чтобы потоки не мешали друг другу. Lockfree - это просто способ вместо этого попытаться ускорить чрезвычайно частые помехи. Пинг-понг на линии кеша трудно избежать в lockfree.   -  person doug65536    schedule 18.05.2016


Ответы (1)


addr aIandF(addr r1) {
  addr tmp;
  int c;
  do {
    do {
      // r1 holds the address of the address
      // of the refcount
      tmp = *r1;       // grab the address of the refcount
      if (!tmp) break; // if it's null, bail

      // read current refcount
      // and acquire reservation
      c = lwarx(tmp);

      // now we hold the reservation,
      // check to see if another thread
      // has changed the shared block address
    } while (tmp != *r1); // if so, start over

    // if the store succeeds we know we held
    // the reservation throughout
  } while (tmp && !stwcx(tmp, c+1));
  return tmp;
};

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

В статье доктора Доббса описывается операция по освобождению ссылки как первая атомарная замена адреса общего счетчика в исходном объекте общего указателя нулевым указателем, локальным для функции; затем атомарное уменьшение счетчика; затем тестирование, чтобы увидеть, был ли результат декремента нулевым. Этот порядок операций важен: вы говорите: «Адрес совместно используемого объекта может никогда не измениться, и LL / SC может быть успешным - но как это нам поможет, если другой поток тем временем освободил разделяемый объект? " - но этого никогда не произойдет, поскольку объект никогда не будет освобожден без предварительной замены, что дает нам возможность наблюдать за изменением адреса.

aIandF проверяет, является ли адрес счетчика нулевым при входе.

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

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

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

РЕДАКТИРОВАТЬ: подумав об этом, алгоритм также, по-видимому, делает предположение, что всегда безопасно читать из адреса памяти, который когда-то был действительным (например, без MMU / защиты; или алгоритм сломан) :

if (!tmp) break;

// another thread could, at this point, do its swap, 
// decrement *and* destroy the object tmp points to
// before we get to do anything else

c = lwarx(tmp); 

// if that happened, we'll detect this fact and do nothing with c
// but ONLY if the lwarx doesn't trap 
// (due to the memory tmp points to 
// getting unmapped when the other thread frees the object)
person moonshadow    schedule 17.05.2016
comment
Хм ... Понятно. Связанная загрузка также вызовет такие вещи, как ошибки Valgrind, если она читает из освобожденной памяти. Похоже, что технически этот алгоритм демонстрирует неопределенное поведение. Я думаю, это можно было бы заставить работать, если бы все узлы / общие объекты / блоки управления были выделены из пула - person Siler; 18.05.2016