weak_ptr lock c++




Quando std:: weak_ptr é útil? (9)

Eu comecei a estudar os ponteiros inteligentes do C ++ 11 e não vejo nenhum uso útil do std::weak_ptr . Alguém pode me dizer quando std::weak_ptr é útil / necessário?


Além dos outros casos de uso válidos já mencionados, std::weak_ptr é uma ferramenta incrível em um ambiente multithreaded, porque

  • Não possui o objeto e, portanto, não pode impedir a exclusão em um segmento diferente
  • std::shared_ptr em conjunto com std::weak_ptr é seguro contra ponteiros pendentes - em oposto a std::unique_ptr em conjunto com ponteiros brutos
  • std::weak_ptr::lock() é uma operação atômica (veja também Sobre thread-safety of weak_ptr )

Considere uma tarefa para carregar todas as imagens de um diretório (~ 10.000) simultaneamente na memória (por exemplo, como um cache de miniaturas). Obviamente, a melhor maneira de fazer isso é um thread de controle, que manipula e gerencia as imagens, e vários threads de trabalho, que carregam as imagens. Agora esta é uma tarefa fácil. Aqui está uma implementação muito simplificada ( join() etc é omitido, os threads teriam que ser tratados de forma diferente em uma implementação real etc)

// a simplified class to hold the thumbnail and data
struct ImageData {
  std::string path;
  std::unique_ptr<YourFavoriteImageLibData> image;
};

// a simplified reader fn
void read( std::vector<std::shared_ptr<ImageData>> imagesToLoad ) {
   for( auto& imageData : imagesToLoad )
     imageData->image = YourFavoriteImageLib::load( imageData->path );
}

// a simplified manager
class Manager {
   std::vector<std::shared_ptr<ImageData>> m_imageDatas;
   std::vector<std::unique_ptr<std::thread>> m_threads;
public:
   void load( const std::string& folderPath ) {
      std::vector<std::string> imagePaths = readFolder( folderPath );
      m_imageDatas = createImageDatas( imagePaths );
      const unsigned numThreads = std::thread::hardware_concurrency();
      std::vector<std::vector<std::shared_ptr<ImageData>>> splitDatas = 
        splitImageDatas( m_imageDatas, numThreads );
      for( auto& dataRangeToLoad : splitDatas )
        m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
   }
};

Mas se torna muito mais complicado, se você quiser interromper o carregamento das imagens, por exemplo, porque o usuário escolheu um diretório diferente. Ou até mesmo se você quiser destruir o gerente.

Você precisaria de uma comunicação de thread e teria que parar todos os threads de loader, antes de poder alterar seu campo m_imageDatas . Caso contrário, os carregadores continuariam carregando até que todas as imagens fossem concluídas - mesmo que já estejam obsoletas. No exemplo simplificado, isso não seria muito difícil, mas em um ambiente real as coisas podem ser muito mais complicadas.

Os encadeamentos provavelmente seriam parte de um conjunto de encadeamentos usado por vários gerenciadores, dos quais alguns estão sendo interrompidos, e alguns não são etc. O parâmetro simples imagesToLoad seria uma fila bloqueada, na qual esses gerentes pressionam suas solicitações de imagem do controle diferente threads com os leitores estourando os pedidos - em uma ordem arbitrária - no outro extremo. E assim a comunicação se torna difícil, lenta e propensa a erros. Uma maneira muito elegante de evitar qualquer comunicação adicional em tais casos é usar std::shared_ptr em conjunto com std::weak_ptr .

// a simplified reader fn
void read( std::vector<std::weak_ptr<ImageData>> imagesToLoad ) {
   for( auto& imageDataWeak : imagesToLoad ) {
     std::shared_ptr<ImageData> imageData = imageDataWeak.lock();
     if( !imageData )
        continue;
     imageData->image = YourFavoriteImageLib::load( imageData->path );
   }
}

// a simplified manager
class Manager {
   std::vector<std::shared_ptr<ImageData>> m_imageDatas;
   std::vector<std::unique_ptr<std::thread>> m_threads;
public:
   void load( const std::string& folderPath ) {
      std::vector<std::string> imagePaths = readFolder( folderPath );
      m_imageDatas = createImageDatas( imagePaths );
      const unsigned numThreads = std::thread::hardware_concurrency();
      std::vector<std::vector<std::weak_ptr<ImageData>>> splitDatas = 
        splitImageDatasToWeak( m_imageDatas, numThreads );
      for( auto& dataRangeToLoad : splitDatas )
        m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
   }
};

Essa implementação é quase tão fácil quanto a primeira, não precisa de nenhuma comunicação de encadeamento adicional e pode fazer parte de um conjunto / fila de encadeamentos em uma implementação real. Como as imagens expiradas são ignoradas e as imagens não expiradas são processadas, os encadeamentos nunca precisariam ser interrompidos durante a operação normal. Você sempre pode mudar o caminho com segurança ou destruir seus gerentes, já que o leitor fn verifica se o ponteiro de propriedade não expirou.


Ao usar ponteiros, é importante entender os diferentes tipos de ponteiros disponíveis e quando faz sentido usar cada um deles. Existem quatro tipos de ponteiros em duas categorias, como segue:

  • Ponteiros brutos:
    • Ponteiro Bruto [ou seja, SomeClass* ptrToSomeClass = new SomeClass(); ]
  • Ponteiros inteligentes:
    • Ponteiros exclusivos [ie std::unique_ptr<SomeClass> uniquePtrToSomeClass ( new SomeClass() ); ]
    • Ponteiros Compartilhados [ie std::shared_ptr<SomeClass> sharedPtrToSomeClass ( new SomeClass() ); ]
    • Ponteiros Fracos [ie std::weak_ptr<SomeClass> weakPtrToSomeWeakOrSharedPtr ( weakOrSharedPtr ); ]

Ponteiros brutos (às vezes chamados de "ponteiros legados" ou "ponteiros C") fornecem um comportamento de ponteiro "básico" e são uma fonte comum de vazamentos de bugs e memória. Os ponteiros brutos não fornecem meios para rastrear a propriedade do recurso e os desenvolvedores devem chamar "excluir" manualmente para garantir que não estejam criando um vazamento de memória. Isso se torna difícil se o recurso for compartilhado, pois pode ser um desafio saber se algum objeto ainda está apontando para o recurso. Por esses motivos, os ponteiros brutos geralmente devem ser evitados e usados ​​apenas em seções críticas de desempenho do código com escopo limitado.

Ponteiros exclusivos são um ponteiro inteligente básico que 'possui' o ponteiro bruto subjacente para o recurso e é responsável por chamar excluir e liberar a memória alocada quando o objeto que 'possui' o ponteiro exclusivo sai do escopo. O nome 'exclusivo' refere-se ao fato de que apenas um objeto pode 'possuir' o ponteiro único em um determinado ponto no tempo. A propriedade pode ser transferida para outro objeto por meio do comando move, mas um ponteiro exclusivo nunca pode ser copiado ou compartilhado. Por esses motivos, os ponteiros únicos são uma boa alternativa aos ponteiros brutos, no caso de apenas um objeto precisar do ponteiro em um determinado momento, e isso alivia o desenvolvedor da necessidade de liberar memória no final do ciclo de vida do objeto proprietário.

Ponteiros compartilhados são outro tipo de ponteiro inteligente que são semelhantes a ponteiros exclusivos, mas permitem que muitos objetos tenham propriedade sobre o ponteiro compartilhado. Como o ponteiro exclusivo, os ponteiros compartilhados são responsáveis ​​por liberar a memória alocada depois que todos os objetos são feitos apontando para o recurso. Isso é feito com uma técnica chamada contagem de referência. Cada vez que um novo objeto apropria-se do ponteiro compartilhado, a contagem de referência é incrementada em um. Da mesma forma, quando um objeto sai do escopo ou para de apontar para o recurso, a contagem de referência é diminuída em um. Quando a contagem de referência atinge zero, a memória alocada é liberada. Por esses motivos, os ponteiros compartilhados são um tipo muito poderoso de ponteiro inteligente que deve ser usado sempre que vários objetos precisarem apontar para o mesmo recurso.

Por fim, os ponteiros fracos são outro tipo de ponteiro inteligente que, em vez de apontar diretamente para um recurso, apontam para outro ponteiro (fraco ou compartilhado). Os ponteiros fracos não podem acessar um objeto diretamente, mas podem dizer se o objeto ainda existe ou se expirou. Um ponteiro fraco pode ser temporariamente convertido em um ponteiro inteligente para acessar o objeto apontado (desde que ele ainda exista). Para ilustrar, considere o seguinte exemplo:

  • Você está ocupado e tem reuniões sobrepostas: Reunião A e Reunião B
  • Você decide ir ao Meeting A e seu colega de trabalho vai ao Meeting B
  • Você diz ao seu colega de trabalho que, se o Meeting B ainda estiver indo após o término do Meeting A, você
  • Os dois cenários a seguir podem ser reproduzidos:
    • A reunião A termina e a Reunião B ainda continua, então você participa
    • A reunião A termina e a Reunião B também terminou, para que você não participe

No exemplo, você tem um ponteiro fraco para a Reunião B. Você não é um "proprietário" na Reunião B, portanto, pode terminar sem você e você não sabe se terminou ou não, a menos que você verifique. Se não terminou, você pode participar e participar, caso contrário, você não pode. Isso é diferente de ter um ponteiro compartilhado para a Reunião B porque você seria um "proprietário" na Reunião A e na Reunião B (participando de ambas ao mesmo tempo).

O exemplo ilustra como um ponteiro fraco funciona e é útil quando um objeto precisa ser um observador externo, mas não quer a responsabilidade de propriedade. Isso é particularmente útil no cenário em que dois objetos precisam apontar um para o outro (também conhecido como referência circular). Com ponteiros compartilhados, nenhum objeto pode ser liberado porque eles ainda são 'fortemente' apontados pelo outro objeto. Com ponteiros fracos, os objetos podem ser acessados ​​quando necessário e liberados quando não precisam mais existir.


Eles são úteis com Boost.Asio quando você não tem a garantia de que um objeto de destino ainda existe quando um manipulador assíncrono é invocado. O truque é ligar um weak_ptr no objeto manipulador assíncrono, usando capturas std::bind ou lambda.

void MyClass::startTimer()
{
    std::weak_ptr<MyClass> weak = shared_from_this();
    timer_.async_wait( [weak](const boost::system::error_code& ec)
    {
        auto self = weak.lock();
        if (self)
        {
            self->handleTimeout();
        }
        else
        {
            std::cout << "Target object no longer exists!\n";
        }
    } );
}

Essa é uma variante do self = shared_from_this() idiomática frequentemente vista nos exemplos Boost.Asio, em que um manipulador assíncrono pendente não prolonga a vida útil do objeto de destino, mas ainda é seguro se o objeto de destino for excluído.


Eu vejo std::weak_ptr<T> como um identificador para um std::shared_ptr<T> : Ele me permite obter o std::shared_ptr<T> se ele ainda existir, mas não irá prolongar sua vida útil. Existem vários cenários quando esse ponto de vista é útil:

// Some sort of image; very expensive to create.
std::shared_ptr< Texture > texture;

// A Widget should be able to quickly get a handle to a Texture. On the
// other hand, I don't want to keep Textures around just because a widget
// may need it.

struct Widget {
    std::weak_ptr< Texture > texture_handle;
    void render() {
        if (auto texture = texture_handle.get(); texture) {
            // do stuff with texture. Warning: `texture`
            // is now extending the lifetime because it
            // is a std::shared_ptr< Texture >.
        } else {
            // gracefully degrade; there's no texture.
        }
    }
};

Outro cenário importante é quebrar ciclos em estruturas de dados.

// Asking for trouble because a node owns the next node, and the next node owns
// the previous node: memory leak; no destructors automatically called.
struct Node {
    std::shared_ptr< Node > next;
    std::shared_ptr< Node > prev;
};

// Asking for trouble because a parent owns its children and children own their
// parents: memory leak; no destructors automatically called.
struct Node {
    std::shared_ptr< Node > parent;
    std::shared_ptr< Node > left_child;
    std::shared_ptr< Node > right_child;
};

// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
    std::shared_ptr< Node > next;
    std::weak_ptr< Node > prev;
};

// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
    std::weak_ptr< Node > parent;
    std::shared_ptr< Node > left_child;
    std::shared_ptr< Node > right_child;
};

Herb Sutter tem uma excelente palestra que explica o melhor uso dos recursos de linguagem (neste caso, os ponteiros inteligentes) para garantir a Leak Freedom by Default (ou seja, tudo se encaixa no local por construção; dificilmente você pode estragar tudo). É um relógio obrigatório.


Outra resposta, espero mais simples. (para colegas googlers)

Suponha que você tenha objetos Team e Member .

Obviamente, é um relacionamento: o objeto Team terá ponteiros para seus Members . E é provável que os membros também tenham um ponteiro de volta para o objeto Team .

Então você tem um ciclo de dependência. Se você usar shared_ptr , os objetos não serão mais liberados automaticamente quando você abandonar a referência a eles, porque eles se referenciam uns aos outros de maneira cíclica. Isso é um vazamento de memória.

Você quebra isso usando weak_ptr . O "proprietário" normalmente usa shared_ptr e o "owned" usa um weak_ptr para seu pai, e converte-o temporariamente para shared_ptr quando precisa acessar seu pai.

Armazene um ptr fraco:

weak_ptr<Parent> parentWeakPtr_ = parentSharedPtr; // automatic conversion to weak from shared

então use quando necessário

shared_ptr<Parent> tempParentSharedPtr = parentWeakPtr_.lock(); // on the stack, from the weak ptr
if( !tempParentSharedPtr ) {
  // yes it may failed if parent was freed since we stored weak_ptr
} else {
  // do stuff
}
// tempParentSharedPtr is released when it goes out of scope

Um bom exemplo seria um cache.

Para objetos acessados ​​recentemente, você quer mantê-los na memória, então você segura um ponteiro forte para eles. Periodicamente, você verifica o cache e decide quais objetos não foram acessados ​​recentemente. Você não precisa guardá-los na memória, para se livrar do ponteiro forte.

Mas e se esse objeto estiver em uso e algum outro código tiver um forte indicador disso? Se o cache se livrar de seu único ponteiro para o objeto, ele nunca poderá encontrá-lo novamente. Assim, o cache mantém um ponteiro fraco para os objetos que precisa encontrar, caso eles permaneçam na memória.

Isto é exatamente o que um ponteiro fraco faz - ele permite que você localize um objeto se ainda estiver por perto, mas não o mantém se nada mais precisar.


std::weak_ptr é uma ótima maneira de resolver o problema do ponteiro pendente . Usando apenas ponteiros brutos, é impossível saber se os dados referenciados foram desalocados ou não. Em vez disso, ao permitir que um std::shared_ptr gerencie os dados e forneça std::weak_ptr aos usuários dos dados, os usuários podem verificar a validade dos dados chamando expired() ou lock() .

Você não poderia fazer isso com std::shared_ptr sozinho, porque todas as instâncias std::shared_ptr compartilham a propriedade dos dados que não são removidos antes que todas as instâncias de std::shared_ptr sejam removidas. Aqui está um exemplo de como verificar o ponteiro pendente usando lock() :

#include <iostream>
#include <memory>

int main()
{
    // OLD, problem with dangling pointer
    // PROBLEM: ref will point to undefined data!

    int* ptr = new int(10);
    int* ref = ptr;
    delete ptr;

    // NEW
    // SOLUTION: check expired() or lock() to determine if pointer is valid

    // empty definition
    std::shared_ptr<int> sptr;

    // takes ownership of pointer
    sptr.reset(new int);
    *sptr = 10;

    // get pointer to data without taking ownership
    std::weak_ptr<int> weak1 = sptr;

    // deletes managed object, acquires new pointer
    sptr.reset(new int);
    *sptr = 5;

    // get pointer to new data without taking ownership
    std::weak_ptr<int> weak2 = sptr;

    // weak1 is expired!
    if(auto tmp = weak1.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak1 is expired\n";

    // weak2 points to new data (5)
    if(auto tmp = weak2.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak2 is expired\n";
}

weak_ptr também é bom para verificar a exclusão correta de um objeto - especialmente em testes de unidade. Caso de uso típico pode ser assim:

std::weak_ptr<X> weak_x{ shared_x };
shared_x.reset();
BOOST_CHECK(weak_x.lock());
... //do something that should remove all other copies of shared_x and hence destroy x
BOOST_CHECK(!weak_x.lock());

shared_ptr : mantém o objeto real.

weak_ptr : usa o lock para se conectar ao proprietário real ou retorna NULL de outra forma.

Grosso modo, o papel de weak_ptr é semelhante ao papel da agência de habitação . Sem agentes, para alugar uma casa, talvez tenhamos que checar casas aleatórias na cidade. Os agentes garantem que visitemos apenas as casas ainda acessíveis e disponíveis para aluguel.







weak-ptr