¿Por qué std::shared ptr:: unique() está obsoleto?


¿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á obsoleta a partir de C++17 porque use_count es solo una aproximación en un entorno multihilo.

entiendo que esto es cierto para use_count() > 1: Mientras estoy sosteniendo una referencia a él, alguien puede, simultáneamente, dejar ir de su o crear una nueva copia.

Pero si use_count() devuelve 1 (que es lo que me interesa cuando llamo a unique()), entonces no hay otro hilo que pueda cambiar ese valor de una manera picante, así que esperaría que esto sea seguro:{[18]]}

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 a C++17 CD comment CA 14, pero no pude encontrar dicho comentario en sí.

Como alternativa, ese documento propuso agregar algunas notas, entre ellas las siguientes: {[18]]}

Nota: Cuando múltiples subprocesos pueden afectar el valor de retorno de use_count(), el resultado debe ser tratado como aproximado. In particular, use_count() == 1 does not imply that accesses through a previously destroyed shared_ptr have in any sense completed. - nota final

Entiendo que este podría ser el caso para la forma use_count() se especifica actualmente (debido a la falta de sincronización garantizada), pero ¿por qué la resolución no se limitó a 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 la hiciera prohibitivamente costosa), entonces ¿cómo es posible implementar correctamente el destructor?

Actualización:

Pasé por alto el caso obvio presentado por @ alexeykuzmin0 y @ rubenvb, porque hasta ahora solo usé unique() en instancias de shared_ptr que eran no es accesible a otros hilos. Así que no había peligro de que esa instancia en particular fuera copiada de una manera picante.

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

A 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() realmente se sincronizaría con los destructores de otras copias)?

Author: Toby Speight, 2016-12-14

3 answers

Creo que P0521R0resuelve potencialmente carrera de datos mediante el mal uso de shared_ptr como sincronización entre hilos. Dice use_count() devuelve un valor de refcount no fiable, y por lo tanto, unique() la función miembro será inútil cuando se multiproceso.

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 sucede-antes de relación del destructor de [D] shared_ptr a [U] llamando a unique(). Así que no podemos esperar relación [W] ⇒ [D] ⇒ [U] ⇒ [R] y [W] ⇒ [R]. ('⇒'denota sucede - antes de la relación).


EDITADO: Encontré dos problemas relacionados con LWG; LWG2434. shared_ptr:: use_count() es eficiente, LWG2776. shared_ptr unique () y use_count () . Es solo una especulación, pero el Comité WG21 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 Notas que su use_count() podría ser ineficiente. Este es un intento de reconocer las implementaciones reflinked (que pueden ser utilizadas por los punteros inteligentes de Loki, por ejemplo). Sin embargo, no hay ninguna implementación shared_ptr que use reflinking, especialmente después de que C++11 reconociera la existencia de multithreading. Todo el mundo usa refcounts atómicos, así que use_count() es solo una carga atómica .

LWG2776 cita (énfasis mío):

El la eliminación de la restricción de "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 synchronize para garantizar que los accesos anteriores a través de otra referencia sean visibles para el llamante exitoso de unique(). Muchas implementaciones actuales utilizan una carga relajada, y no proporcionan esta garantía, ya que no está indicada en el estándar. Para el uso de depuración/sugerencia que estaba bien. Sin ella la especificación es poco claro y probablemente engañoso.

[...]

Preferiría especificar use_count() como solo proporcionar una pista poco fiable de la cuenta real (otra forma de decir solo depurar). O desaprobarlo, como JF sugirió. No podemos hacer use_count() fiable sin añadir sustancialmente más esgrima. Realmente no queremos que alguien espere use_count() == 2 para determinar que otro hilo llegó tan lejos. Y desafortunadamente, no creo que actualmente digamos nada para dejar claro que es un error.

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

 7
Author: yohjp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-16 01:41:30

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 de if.

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

 12
Author: alexeykuzmin0,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-14 12:20:36

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

Este documento contiene todos los comentarios NB (Organismo Nacional) para la reunión de Issaquah. CA 14 dice:

La eliminación de la restricción "debug only" para use_count () y unique () en shared_ptr introdujo un error: para que unique () producir un valor útil y confiable, necesita una cláusula synchronize para asegurarse de que los accesos anteriores a través de otra referencia son visible a la persona que llama con éxito a unique (). Muchas implementaciones actuales utilizan un carga relajada, y no proporcionan esta garantía, ya que no se indica en el Estándar. Para el uso de depuración / sugerencia que estaba bien. Sin ella el la especificación es confusa y engañosa.

 3
Author: Mikel F,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-14 18:01:36