В общем, наиболее широко известные реализации ссылок на классы smart ptr в С++, включая стандартный std::shared_ptr
, используют подсчет атомных ссылок, но не обеспечивают атомный доступ к одному и тому же экземпляру smart ptr. Другими словами, несколько потоков могут безопасно работать с отдельными экземплярами shared_ptr
, которые указывают на один и тот же общий объект, но несколько потоков не могут безопасно читать/записывать экземпляры одного и того же экземпляра shared_ptr
без предоставления какой-либо синхронизации, такой как мьютекс или что угодно.
Атомная версия shared_ptr
, называемая "atomic_shared_ptr
", была предложена, а предварительная уже существуют. Предположительно, atomic_shared_ptr
может быть легко реализован с помощью блокировки спина или мьютекса, но также возможна блокировка.
После изучения некоторых из этих реализаций одно очевидно: реализация блокировки std::shared_ptr
очень сложна и, по-видимому, требует так много операций compare_and_exchange
, чтобы я поставил вопрос, достигнет ли простая блокировка спина производительность.
Основная причина, по которой так сложно реализовать указатель на подсчет ссылок без блокировки, - это из-за гонки, которая всегда существует между чтением разделяемого блока управления (или самого совместно используемого объекта, если мы говорим об интрузивной совместной указатель) и изменение счетчика ссылок.
Другими словами, вы даже не можете безопасно прочитать счетчик ссылок, потому что никогда не знаете, когда какой-либо другой поток освободил память, в которой живет счетчик ссылок.
Таким образом, в целом для создания незакрепленных версий используются различные сложные стратегии. Реализация здесь выглядит так: она использует стратегию подсчета двойных ссылок, где есть "локальные" ссылки, которые подсчитывают количество потоков, одновременно обращающихся к экземпляру shared_ptr
, и затем "общие" или "глобальные" ссылки, которые подсчитывают количество экземпляров shared_ptr, указывающих на общий объект.
Учитывая всю эту сложность, я был очень удивлен, обнаружив статью доктора Доббса, начиная с 2004 года, не менее (путь до С++ 11 atomics), который, как представляется, небрежно решает всю эту проблему:
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 может быть успешным, но как это нам поможет, если другой поток освободил общий объект за время?