weak_ptr smart shared_ptr pointer make_unique example create c++ multithreading shared-ptr c++17

c++ - smart - ¿Por qué está std:: shared_ptr:: unique() en desuso?



unique pointer (3)

¿Cuál es el problema técnico con std::shared_ptr::unique() que es la razón de su desaprobación en C ++ 17?

Según cppreference.com , std::shared_ptr::unique() está en desuso en C ++ 17 como

esta función está en desuso a partir de C ++ 17 porque use_count es solo una aproximación en un entorno de subprocesos múltiples.

Entiendo que esto es cierto para use_count() > 1 : Mientras mantengo una referencia a él, alguien más puede dejarlo o crear una nueva copia.

Pero si use_count() devuelve 1 (que es lo que me interesa al llamar a unique() ), entonces no hay otro hilo que pueda cambiar ese valor de forma sucia, por lo que espero que esto sea seguro:

if (myPtr && myPtr.unique()) { //Modify *myPtr }

Resultados de mi propia búsqueda:

Encontré este documento: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0521r0.html que propone la desaprobación en respuesta al comentario de CD C ++ 17 CA 14 , pero No se pudo encontrar dicho comentario en sí.

Como alternativa, ese documento propuso agregar algunas notas que incluyen lo siguiente:

Nota: cuando varios subprocesos pueden afectar el valor de retorno de use_count() , el resultado se debe tratar como aproximado. En particular, use_count() == 1 no implica que los accesos a través de un shared_ptr previamente destruido se hayan completado en ningún sentido. - nota final

Entiendo que este podría ser el caso para la forma en que se especifica use_count() actualmente (debido a la falta de sincronización garantizada), pero ¿por qué la resolución no fue solo para especificar dicha sincronización y, por lo tanto, hacer que el patrón anterior sea seguro? Si hubiera una limitación fundamental que no permitiera tal sincronización (o que la hiciera prohibitivamente costosa), ¿cómo es posible implementar correctamente el destructor?

Actualizar:

Pasé por alto el caso obvio presentado por @ alexeykuzmin0 y @rubenvb, porque hasta el momento solo usaba unique() en instancias de shared_ptr que no eran accesibles a otros subprocesos. Así que no había peligro de que esa instancia en particular fuera copiada de una manera cruel.

Todavía me interesaría saber de qué se trataba exactamente CA 14, porque creo que todos mis casos de uso para unique() funcionarán siempre y cuando se garantice que se sincronizarán con lo que suceda con diferentes instancias de shared_ptr en otros subprocesos. Así que todavía me parece una herramienta útil, pero podría pasar por alto algo fundamental aquí.

Para ilustrar lo que tengo en mente, considere lo siguiente:

class MemoryCache { public: MemoryCache(size_t size) : _cache(size) { for (auto& ptr : _cache) { ptr = std::make_shared<std::array<uint8_t, 256>>(); } } // the returned chunk of memory might be passed to a different thread(s), // but the function is never accessed from two threads at the same time std::shared_ptr<std::array<uint8_t,256>> getChunk() { auto it = std::find_if(_cache.begin(), _cache.end(), [](auto& ptr) { return ptr.unique(); }); if (it != _cache.end()) { //memory is no longer used by previous user, so it can be given to someone else return *it; } else { return{}; } } private: std::vector<std::shared_ptr<std::array<uint8_t, 256>>> _cache; };

¿Hay algo malo en ello (si unique() realidad se sincronizaría con los destructores de otras copias)?


Considere el siguiente código:

// global variable std::shared_ptr<int> s = std::make_shared<int>(); // thread 1 if (s && s.unique()) { // modify *s } // thread 2 auto s2 = s;

Aquí tenemos una condición de carrera clásica: s2 puede (o no) crearse como una copia de s en el hilo 2, mientras que el hilo 1 está dentro del if .

shared_ptr unique() == true significa que nadie tiene un shared_ptr apunta a la misma memoria, pero no significa que ningún otro subproceso no tenga acceso a shared_ptr inicial directamente oa través de punteros o referencias.


Para su placer visual: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0488r0.pdf

Este documento contiene todos los comentarios de NB (organismo nacional) para la reunión de Issaquah. CA 14 lee:

La eliminación de la restricción "solo depuración" para use_count () y unique () en shared_ptr introdujo un error: para que unique () produzca un valor útil y confiable, necesita una cláusula de sincronización para garantizar que los accesos anteriores a través de otra referencia son visibles para el llamador exitoso de unique (). Muchas implementaciones actuales utilizan una carga relajada, y no proporcionan esta garantía, ya que no se establece en la Norma. Para el uso de debug / hint que estaba bien. Sin ella la especificación es confusa y engañosa.


Pienso que http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0521r0.html resuelve potencialmente la carrera de datos al usar indebidamente shared_ptr como sincronización entre subprocesos. Dice que use_count() devuelve un valor de refcount no confiable y, por lo tanto, la función de miembro unique() será inútil cuando se realice multihilo.

int main() { int result = 0; auto sp1 = std::make_shared<int>(0); // refcount: 1 // Start another thread std::thread another_thread([&result, sp2 = sp1]{ // refcount: 1 -> 2 result = 42; // [W] store to result // [D] expire sp2 scope, and refcount: 2 -> 1 }); // Do multithreading stuff: // Other threads may concurrently increment/decrement refcounf. if (sp1.unique()) { // [U] refcount == 1? assert(result == 42); // [R] read from result // This [R] read action cause data race w.r.t [W] write action. } another_thread.join(); // Side note: thread termination and join() member function // have happens-before relationship, so [W] happens-before [R] // and there is no data race on following read action. assert(result == 42); }

La función miembro unique() no tiene ningún efecto de sincronización y no hay una relación de shared_ptr del destructor de [D] shared_ptr a [U] llamando a unique() . Entonces no podemos esperar relación [W] ⇒ [D] ⇒ [U] ⇒ [R] y [W] ⇒ [R]. (''⇒'' denota una relación de suceso-antes).

EDITADO: Encontré dos problemas relacionados con el LWG; LWG2434. shared_ptr :: use_count () es eficiente , LWG2776. shared_ptr unique () y use_count () . Es solo una especulación, pero el Comité WG21 le da prioridad a la implementación existente de la biblioteca estándar de C ++, por lo que codifican su comportamiento en C ++ 1z.

LWG2434 cita (énfasis mío):

shared_ptr y weak_ptr tienen Notas que su use_count() podría ser ineficiente. Este es un intento de reconocer las implementaciones por reflexión (que pueden ser utilizadas por los punteros inteligentes de Loki, por ejemplo). Sin embargo, no hay implementaciones de shared_ptr que utilicen la reflexión , especialmente después de que C ++ 11 reconoció la existencia de multithreading. Todos usan refcounts atómicos, por lo que use_count() es solo una carga atómica .

LWG2776 cita (énfasis mío):

La eliminación de la restricción "solo depuración" para use_count() y unique() en shared_ptr por LWG 2434 introdujo un error. Para que unique() produzca un valor útil y confiable, necesita una cláusula de sincronización para garantizar que los accesos anteriores a través de otra referencia sean visibles para el llamador exitoso de unique() . Muchas implementaciones actuales utilizan una carga relajada, y no proporcionan esta garantía, ya que no se establece en la norma. Para el uso de debug / hint que estaba bien. Sin ella, la especificación no es clara y probablemente sea engañosa.

[...]

Preferiría especificar use_count() ya que solo proporciona un indicio poco confiable del conteo real (otra forma de decir depuración solamente). O desapruebe, como sugirió JF. No podemos hacer que use_count() confiable sin agregar sustancialmente más cercas. Realmente no queremos que alguien que espera a use_count() == 2 determine que otro hilo llegó tan lejos. Y desafortunadamente, no creo que actualmente digamos nada para dejar en claro que es un error.

Esto implicaría que use_count() normalmente usa memory_order_relaxed , y que unique no se especifica ni se implementa en términos de use_count() .