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 unshared_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
yweak_ptr
tienen Notas que suuse_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 deshared_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 queuse_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()
yunique()
enshared_ptr
por LWG 2434 introdujo un error. Para queunique()
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 deunique()
. 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 queuse_count()
confiable sin agregar sustancialmente más cercas. Realmente no queremos que alguien que espera ause_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 usamemory_order_relaxed
, y que unique no se especifica ni se implementa en términos deuse_count()
.