c++ c++11 concurrency atomic smart-pointers

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 de shared_ptr , enseñamos a los programadores a usar tipos atómicos en C ++, no atomic_* 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ón atomic_* 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 de atomic_load(&head) ), de modo que en este estilo cada uso de la variable es "incorrecto por defecto". Si olvida escribir la llamada atomic_* 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 variable atomic , 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ícitos memory_order_* , y generalmente reinterpret_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 un atomic_flag (o similar) adicional para el spinlock interno como es habitual en atomic<bigstruct> . En contraste, se requiere que las funciones independientes existentes sean utilizables en cualquier objeto arbitrario shared_ptr , a pesar de que la gran mayoría de shared_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 que shared_ptr lleve la sobrecarga de una variable interna de spinlock (mejor concurrencia, pero una sobrecarga significativa por shared_ptr ), o bien la biblioteca debe mantener una estructura de datos de lookaside para almacenar la información adicional para shared_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.