Мне интересно, можно ли создать безопасный, потокобезопасный общий указатель для любой из "общих" архитектур, таких как x64 или ARMv7/ARMv8.
Говоря о программировании без блокировки на cppcon2014, Херб Саттер представил (частичную) реализацию блокировки без привязки к списку, Реализация выглядит довольно просто, но она опирается на атомную реализацию shared_ptr
, которая еще не существует в стандартной библиотеке, или на использование специализированных функций std::atomic...
. Это особенно важно, поскольку одиночные push/pop-вызовы потенциально вызывают множественные атомные нагрузки/хранилища и операции compare_exchange
.
Проблема, которую я вижу (и я думаю, что некоторые из вопросов в разговоре идут в одном направлении) заключается в том, что для того, чтобы это была фактическая структура данных без блокировки, эти атомные операции должны были бы быть заблокированы сами. Я не знаю никакой стандартной реализации библиотеки для std::atomic...
функций, которые не блокируются и, по крайней мере, с коротким поиском google/SO, я также не нашел предложения о том, как реализовать специальную функцию блокировки для std::atomic<std::shared_ptr>
.
Теперь, прежде чем я трачу свое время на это, я хотел спросить:
- Знаете ли вы, если можно вообще написать бездепозитный, атомный общий указатель?
- Есть ли уже какие-либо реализации, которые я забыл и, в идеале, даже совместимы с тем, что вы ожидаете от
std::atomic<std::shared_ptr>
? Для указанной очереди особенно требуетсяCAS
-операция. - Если нет способа реализовать это на существующих архитектурах, вы видите какое-либо другое преимущество в реализации Herb по сравнению с "нормальным" связанным списком, который защищен блокировкой?
Для справки, вот код от Herb Sutter (может содержать опечатки от меня):
template<class T>
class slist {
struct Node { T t; std::shared_ptr<Node> next; };
std::atomic<std::shared_ptr<Node>> head;
public:
class reference{
std::shared_ptr<Node> p;
public:
reference(std::shared_ptr<Node> p_){}
T& operator*(){ return p->t; }
T* operator->(){ return &p->t; }
};
auto find(T t) const {
auto p = head.load();
while (p && p-> != t) {
p = p - next;
}
return reference(move(p));
}
void push_front(T t) {
auto p = std::make_shared<Node>();
p->t = t;
p->next = head;
while (!head.compare_exchange_weak(p->next, p)) {}
}
void pop_front() {
auto p = head.load();
while (p && !head.compare_exchange_weak(p, p - next)) { ; }
}
};
Обратите внимание, что в этой реализации отдельные экземпляры shared_ptr
могут быть доступны/изменены несколькими различными потоками. Его можно прочитать/скопировать, reset и даже удалить (как часть node). Таким образом, это не связано с тем, что несколько разных объектов shared_ptr
(которые управляют одним и тем же объектом) могут использоваться несколькими потоками без условия гонки - это уже верно для текущих реализаций и требуется стандартом, но речь идет о одновременном доступе к один экземпляр указателя, который является - для стандартных общих указателей - не более поточным, чем те же операции над необработанными указателями.
Чтобы объяснить мою мотивацию:
Это в основном академический вопрос. Я не собираюсь реализовывать свой собственный список блокировки в производственном коде, но я нахожу тему интересной и на первый взгляд, презентация Herb показалась хорошим введением. Однако, думая о этом вопросе и @sehe комментировать мой ответ, я вспомнил этот разговор, посмотрел на него и понял, что не имеет смысла вызовите реализацию Herb lock-free, если для примитивных операций требуются блокировки (которые они в настоящее время делают). Поэтому мне было интересно, является ли это лишь ограничением существующих реализаций или фундаментальным недостатком дизайна.