c++ - ¿Cuál es la diferencia entre std:: shared_ptr y std:: experimental:: atomic_shared_ptr?
c++11 concurrency (4)
Leí el following artículo de Antony Williams y, como entendí, además del recuento atómico compartido en std::shared_ptr
en std::experimental::atomic_shared_ptr
el puntero real al objeto compartido también es atómico?
Pero cuando leí acerca de la versión de referencia de lock_free_stack
descrita en el libro de Antony sobre C ++ Concurrency, me parece que los mismos aplies también para std::shared_ptr
, porque funciones como std::atomic_load
, std::atomic_compare_exchnage_weak
se aplican a las instancias de std::shared_ptr
.
template <class T>
class lock_free_stack
{
public:
void push(const T& data)
{
const std::shared_ptr<node> new_node = std::make_shared<node>(data);
new_node->next = std::atomic_load(&head_);
while (!std::atomic_compare_exchange_weak(&head_, &new_node->next, new_node));
}
std::shared_ptr<T> pop()
{
std::shared_ptr<node> old_head = std::atomic_load(&head_);
while(old_head &&
!std::atomic_compare_exchange_weak(&head_, &old_head, old_head->next));
return old_head ? old_head->data : std::shared_ptr<T>();
}
private:
struct node
{
std::shared_ptr<T> data;
std::shared_ptr<node> next;
node(const T& data_) : data(std::make_shared<T>(data_)) {}
};
private:
std::shared_ptr<node> head_;
};
¿Cuál es la diferencia exacta entre estos dos tipos de punteros inteligentes, y si el puntero en la instancia std::shared_ptr
no es atómico, por qué es posible la implementación de pila libre de bloqueo anterior?
La "cosa" atómica en shared_ptr
no es el propio puntero compartido, sino el bloque de control al que apunta. lo que significa que mientras no shared_ptr
el shared_ptr
través de múltiples subprocesos, estás bien. tenga en cuenta que copiar un shared_ptr
solo muta el bloque de control, y no el shared_ptr
sí.
std::shared_ptr<int> ptr = std::make_shared<int>(4);
for (auto i =0;i<10;i++){
std::thread([ptr]{ auto copy = ptr; }).detach(); //ok, only mutates the control block
}
Mutar el puntero compartido, como asignarle diferentes valores a varios subprocesos, es una carrera de datos, por ejemplo:
std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::thread threadA([&ptr]{
ptr = std::make_shared<int>(10);
});
std::thread threadB([&ptr]{
ptr = std::make_shared<int>(20);
});
Aquí, estamos mutando el bloque de control (que está bien) pero también el puntero compartido, haciendo que apunte a valores diferentes de varios subprocesos. Esto no está bien.
Una solución a ese problema es envolver el shared_ptr
con un bloqueo, pero esta solución no es tan escalable en algunos conflictos y, en cierto sentido, pierde la sensación automática del puntero compartido estándar.
Otra solución es usar las funciones estándar que citó, como std::atomic_compare_exchange_weak
. Esto hace que el trabajo de sincronización de punteros compartidos sea manual, lo que no nos gusta.
Aquí es donde entra el puntero compartido atómico. Puede mutar el puntero compartido desde varios subprocesos sin temer una carrera de datos y sin usar ningún bloqueo. Las funciones independientes serán las de miembros, y su uso será mucho más natural para el usuario. Este tipo de puntero es extremadamente útil para estructuras de datos sin bloqueo.
Llamar a std::atomic_load()
o std::atomic_compare_exchange_weak()
en shared_ptr
es funcionalmente equivalente a llamar a atomic_shared_ptr::load()
o atomic_shared_ptr::atomic_compare_exchange_weak()
. No debería haber ninguna diferencia de rendimiento entre los dos. Llamar a std::atomic_load()
o std::atomic_compare_exchange_weak()
en un atomic_shared_ptr
sería sintácticamente redundante y podría o no incurrir en una penalización de rendimiento.
atomic_shared_ptr
es un refinamiento de API. shared_ptr
ya admite operaciones atómicas, pero solo cuando se usan las funciones atómicas no miembros apropiadas. Esto es propenso a errores, porque las operaciones no atómicas permanecen disponibles y son demasiado fáciles de invocar por accidente para un programador incauto. atomic_shared_ptr
es menos propenso a errores porque no expone ninguna operación no atómica.
shared_ptr
y atomic_shared_ptr
exponen diferentes API, pero no necesariamente tienen que implementarse de manera diferente; shared_ptr
ya admite todas las operaciones expuestas por atomic_shared_ptr
. Dicho esto, las operaciones atómicas de shared_ptr
no son tan eficientes como podrían ser, ya que también deben soportar operaciones no atómicas. Por lo tanto, hay razones de rendimiento por las que atomic_shared_ptr
podría implementarse de manera diferente. Esto está relacionado con el principio de responsabilidad única. "Una entidad con varios propósitos dispares (...) a menudo ofrece interfaces inutilizadas para cualquiera de sus propósitos específicos porque la superposición parcial entre varias áreas de funcionalidad desdibuja la visión necesaria para implementar cada uno de manera precisa". (Sutter y Alexandrescu 2005, estándares de codificación C ++ )
N4162 (pdf) , la propuesta de punteros inteligentes atómicos, tiene una buena explicación. Aquí hay una cita de la parte relevante:
Consistencia Por lo que sé, las funciones [util.smartptr.shared.atomic] son las únicas operaciones atómicas en el estándar que no están disponibles a través de un tipo
atomic
. Y para todos los tipos, además deshared_ptr
, enseñamos a los programadores a usar tipos atómicos en C ++, noatomic_*
C-style. Y eso es en parte debido a ...Corrección El uso de las funciones gratuitas hace que el código sea propenso a errores y de forma predeterminada. Es muy superior escribir
atomic
una vez en la propia declaración de variables y saber que todos los accesos serán atómicos, en lugar de tener que acordarse de usar la operaciónatomic_*
en cada uso del objeto, incluso en lecturas aparentemente simples. El último estilo es propenso a errores; por ejemplo, "hacerlo mal" significa simplemente escribir espacios en blanco (p. ej.,head
lugar deatomic_load(&head)
), de modo que en este estilo cada uso de la variable es "incorrecto por defecto". Si olvida escribir la llamadaatomic_*
incluso en un lugar, su código aún se compilará correctamente sin ningún error o advertencia, "parecerá que funciona", incluida la mayoría de las pruebas, pero aún así contendrá una carrera silenciosa con un comportamiento indefinido que generalmente aparece como intermitente, difícil de reproducir. fallas, a menudo / generalmente en el campo, y espero también en algunos casos vulnerabilidades explotables. Estas clases de errores se eliminan simplemente declarando la variableatomic
, porque entonces es seguro por defecto y escribir el mismo conjunto de errores requiere un código explícito que no sea un espacio en blanco (a veces argumentos explícitosmemory_order_*
, y generalmentereinterpret_cast
ing).Rendimiento
atomic_shared_ptr<>
como un tipo distinto tiene una importante ventaja de eficiencia sobre las funciones en [util.smartptr.shared.atomic] - simplemente puede almacenar unatomic_flag
(o similar) adicional para el spinlock interno como es habitual enatomic<bigstruct>
. En contraste, se requiere que las funciones independientes existentes sean utilizables en cualquier objeto arbitrarioshared_ptr
, a pesar de que la gran mayoría deshared_ptr
s nunca se usarán atómicamente. Esto hace que las funciones libres sean inherentemente menos eficientes; por ejemplo, la implementación podría requerir queshared_ptr
lleve la sobrecarga de una variable interna de spinlock (mejor concurrencia, pero una sobrecarga significativa porshared_ptr
), o bien la biblioteca debe mantener una estructura de datos de lookaside para almacenar la información adicional parashared_ptr
que en realidad usada atómicamente, o (lo peor y aparentemente común en la práctica) la biblioteca debe usar un spinlock global.