Um padrão de contagem de referência para idiomas gerenciados por memória?

11

Java e .NET têm coletores de lixo maravilhosos que gerenciam a memória para você e padrões convenientes para liberar rapidamente objetos externos ( Closeable, IDisposable), mas apenas se pertencerem a um único objeto. Em alguns sistemas, um recurso pode precisar ser consumido independentemente por dois componentes e somente ser liberado quando os dois componentes liberarem o recurso.

No C ++ moderno, você resolveria esse problema com a shared_ptr, que deterministicamente liberaria o recurso quando todos os shared_ptrfossem destruídos.

Existem padrões comprovados e documentados para gerenciar e liberar recursos caros que não possuem um único proprietário em sistemas de coleta de lixo não-deterministicamente orientados a objetos?

Cruz
fonte
1
Você já viu a contagem automática de referência de Clang , também usada no Swift ?
JSCs
1
@ Josh Caswell Sim, e isso resolveria o problema, mas estou trabalhando em um espaço coletado de lixo.
C. Ross
8
A contagem de referência é uma estratégia de coleta de lixo.
Jörg W Mittag

Respostas:

15

Em geral, você evita ter um único proprietário - mesmo em idiomas não gerenciados.

Mas o princípio é o mesmo para idiomas gerenciados. Em vez de fechar imediatamente o recurso caro em um, Close()você diminui um contador (incrementado em Open()/ Connect()/ etc) até atingir 0; nesse ponto, o fechamento realmente faz o fechamento. Provavelmente será semelhante ao Flyweight Pattern.

Telastyn
fonte
Era isso que eu estava pensando também, mas existe um padrão documentado para isso? O peso da mosca é certamente semelhante, mas especificamente para a memória, como é geralmente definida.
C. Ross
@ C.Ross Este parece ser um caso em que os finalizadores são incentivados. Você pode usar uma classe de wrapper em torno do recurso não gerenciado, adicionando um finalizador a essa classe para liberar o recurso. Você também pode implementá-lo IDisposable, manter as contagens para liberar o recurso o mais rápido possível, etc. Provavelmente a melhor coisa, muitas vezes, é ter todos os três, mas o finalizador é provavelmente a parte mais crítica, e a IDisposableimplementação é o menos crítico.
Panzercrisis 12/12
11
@ Panzercrisis, exceto que os finalizadores não têm garantia de execução e, principalmente, não garantem a execução imediata .
Caleth
Caleth @ Eu estava pensando que a coisa conta ajudaria com a parte da prontidão. Na medida em que eles não estão funcionando, você quer dizer que o CLR pode simplesmente não resolver isso antes do término do programa, ou você pode ser desqualificado imediatamente?
Panzercrisis
14

Em uma linguagem de coleta de lixo (onde o GC não é determinístico), não é possível vincular com segurança a limpeza de um recurso que não seja a memória ao tempo de vida de um objeto: Não é possível indicar quando um objeto será excluído. O fim da vida útil é inteiramente a critério do coletor de lixo. O GC garante apenas que um objeto permanecerá enquanto estiver acessível. Quando um objeto se torna inacessível, ele pode ser limpo em algum momento no futuro, o que pode envolver a execução de finalizadores.

O conceito de "propriedade de recursos" não se aplica realmente ao idioma do GC. O sistema GC possui todos os objetos.

O que essas linguagens oferecem com try-with-resource + Closeable (Java), usando declarações + IDisposable (C #) ou com declarações + gerenciadores de contexto (Python) é uma maneira de o fluxo de controle (! = Objetos) manter um recurso que é fechado quando o fluxo de controle deixa um escopo. Em todos esses casos, isso é semelhante a um inserido automaticamente try { ... } finally { resource.close(); }. A vida útil do objeto que representa o recurso não está relacionada à vida útil do recurso: o objeto pode continuar ativo após o fechamento do recurso e o objeto pode se tornar inacessível enquanto o recurso ainda está aberto.

No caso de variáveis ​​locais, essas abordagens são equivalentes ao RAII, mas precisam ser usadas explicitamente no site da chamada (ao contrário dos destruidores C ++, que serão executados por padrão). Um bom IDE avisa quando isso é omitido.

Isso não funciona para objetos que são referenciados em locais diferentes de variáveis ​​locais. Aqui, é irrelevante se há uma ou mais referências. É possível converter a referência de recursos por meio de referências a objetos para a propriedade por meio de fluxo de controle, criando um encadeamento separado que retém esse recurso, mas os encadeamentos também são recursos que precisam ser descartados manualmente.

Em alguns casos, é possível delegar a propriedade do recurso a uma função de chamada. Em vez de objetos temporários referenciarem recursos que eles devem (mas não podem) limpar de maneira confiável, a função de chamada contém um conjunto de recursos que precisam ser limpos. Isso funciona apenas até a vida útil de qualquer um desses objetos sobreviver à vida útil da função e, portanto, faz referência a um recurso que já foi fechado. Isso não pode ser detectado por um compilador, a menos que o idioma possua rastreamento de propriedade semelhante ao Rust (nesse caso, já existem soluções melhores para esse problema de gerenciamento de recursos).

Isso deixa como a única solução viável: gerenciamento manual de recursos, possivelmente implementando a contagem de referência. Isso é propenso a erros, mas não impossível. Em particular, ter que pensar em propriedade é incomum nas linguagens de GC; portanto, o código existente pode não ser suficientemente explícito sobre as garantias de propriedade.

amon
fonte
3

Muita informação boa das outras respostas.

Ainda assim, para ser explícito, o padrão que você pode estar procurando é o uso de pequenos objetos de propriedade individual para a construção de fluxo de controle do tipo RAII via usinge IDispose, em conjunto com um objeto (maior, possivelmente contado por referência) que contém alguns (operacionais). sistema).

Portanto, existem os pequenos objetos proprietários não compartilhados que (por meio da construção do objeto menor IDisposee do usingfluxo de controle) podem, por sua vez, informar o objeto compartilhado maior (talvez customizado Acquiree Releasemétodos).

(Os métodos Acquiree Releasemostrados abaixo também estão disponíveis fora do construto using, mas sem a segurança do tryimplícito em using.)


Um exemplo em c #

void Test ( MyRefCountedClass myObj )
{
    using ( var usingRef = myObj.Acquire () )
    {
        var item = usingRef.Item;
        item.SomeMethod ();

        // the `using` automatically invokes Dispose() on usingRef
        //  which in turn invokes Release() on `myObj.
    }
}

interface IReferencable<T> where T: IReferencable<T> {
    Reference<T> Acquire ();
    void Release();
}

struct Reference<T>: IDisposable where T: IReferencable<T>
{
    public readonly T Item;
    public Reference(T item) { Item = item; _released = false; }
    public void Dispose() { if (! _released ) { _released = true; Item.Release(); } }
    private bool _released;
}

class MyRefCountedClass : IReferencable<MyRefCountedClass>
{
    private int _refCount = 0;

    public Reference<MyRefCountedClass> Acquire ()
    {
        _refCount++;
        return new Reference<MyRefCountedClass>(this);
    }

    public void Release ()
    {
        if (--_refCount <= 0)
            Dispose();
    }

    // NOTE that MyRefCountedClass does not have to implement IDisposable, but it can...
    // as shown here it doesn't implement the interface
    private void Dispose ()  
    {
        if ( _refCount > 0 )
            throw new Exception ("Dispose attempted on item in use.");
        // release other resources...
    }

    public int SomeMethod()
    {
        return 0;
    }
}
Erik Eidt
fonte
Se for C # (com o que parece), sua implementação de Referência <T> está sutilmente incorreta. O contrato para os IDisposable.Disposeestados que chamam Disposevárias vezes no mesmo objeto deve ser não operacional. Se eu implementasse esse padrão, também me tornaria Releaseprivado para evitar erros desnecessários e usar delegação em vez de herança (remova a interface, forneça uma SharedDisposableclasse simples que possa ser usada com Disposables arbitrários), mas essas são mais questões de gosto.
Voo
@ Voo, ok, bom ponto, thx!
Erik Eidt
1

A grande maioria dos objetos em um sistema geralmente deve se encaixar em um dos três padrões:

  1. Objetos cujo estado nunca mudará e para os quais as referências são mantidas apenas como um meio de encapsular o estado. As entidades que possuem referências nem sabem nem se importam se outras entidades possuem referências ao mesmo objeto.

  2. Objetos que estão sob o controle exclusivo de uma única entidade, que é o único proprietário de todo estado, e usa o objeto apenas como um meio de encapsular o estado (possivelmente mutável) nele.

  3. Objetos pertencentes a uma única entidade, mas que outras entidades têm permissão para usar de maneiras limitadas. O proprietário do objeto pode usá-lo não apenas como um meio de encapsular o estado, mas também encapsular um relacionamento com as outras entidades que o compartilham.

O rastreamento da coleta de lixo funciona melhor do que a contagem de referência para o número 1, porque o código que usa esses objetos não precisa fazer nada de especial quando é feito com a última referência restante. A contagem de referência não é necessária para o número 2 porque os objetos terão exatamente um proprietário e saberão quando não precisar mais do objeto. O cenário 3 pode apresentar alguma dificuldade se o proprietário de um objeto o matar enquanto outras entidades ainda mantêm referências; mesmo lá, um GC de rastreamento pode ser melhor do que a contagem de referências para garantir que as referências a objetos mortos permaneçam identificáveis ​​de maneira confiável como referências a objetos mortos, enquanto existirem essas referências.

Existem algumas situações em que pode ser necessário que um objeto compartilhável e sem proprietário adquira e retenha recursos externos enquanto alguém precisar de seus serviços, e deve liberá-los quando seus serviços não forem mais necessários. Por exemplo, um objeto que encapsula o conteúdo de um arquivo somente leitura pode ser compartilhado e usado por muitas entidades simultaneamente, sem que nenhuma delas precise saber ou se preocupar com a existência uma da outra. Tais circunstâncias são raras, no entanto. A maioria dos objetos terá um único proprietário claro ou não terá proprietário. A propriedade múltipla é possível, mas raramente é útil.

supercat
fonte
0

Propriedade compartilhada raramente faz sentido

Essa resposta pode ser um pouco fora da tangente, mas tenho que perguntar: quantos casos faz sentido do ponto de vista do usuário final compartilhar a propriedade ? Pelo menos nos domínios em que trabalhei, praticamente não havia, porque, caso contrário, isso implicaria que o usuário não precisaria simplesmente remover algo de uma só vez de um lugar, mas explicitamente removê-lo de todos os proprietários relevantes antes que o recurso fosse realmente removido do sistema.

Geralmente, é uma ideia de engenharia de nível inferior impedir que os recursos sejam destruídos enquanto outra coisa ainda está acessando, como outro encadeamento. Freqüentemente, quando um usuário solicita fechar / remover / excluir algo do software, ele deve ser removido o mais rápido possível (sempre que for seguro removê-lo), e certamente não deve demorar e causar um vazamento de recursos pelo tempo que for necessário. o aplicativo está sendo executado.

Como exemplo, um ativo de jogo em um videogame pode fazer referência a um material da biblioteca de materiais. Certamente, não queremos, digamos, um travamento de ponteiro oscilante se o material for removido da biblioteca de materiais em um segmento enquanto outro segmento ainda estiver acessando o material referenciado pelo ativo do jogo. Mas isso não significa que faz sentido que os recursos do jogo compartilhem a propriedade dos materiais que eles fazem referência com a biblioteca de materiais. Não queremos forçar o usuário a remover explicitamente o material da biblioteca de materiais e ativos. Queremos apenas garantir que os materiais não sejam removidos da biblioteca de materiais, o único proprietário sensato dos materiais, até que outras linhas tenham terminado de acessar o material.

Vazamentos de recursos

No entanto, trabalhei com uma equipe anterior que adotou a GC para todos os componentes do software. E enquanto isso realmente ajudou a garantir que nunca tivéssemos recursos destruídos enquanto outros threads ainda os estavam acessando, acabamos recebendo nossa parcela de vazamentos de recursos .

E esses não foram vazamentos triviais de recursos que perturbam apenas os desenvolvedores, como um kilobyte de memória vazada após uma sessão de uma hora. Foram vazamentos épicos, geralmente gigabytes de memória durante uma sessão ativa, levando a relatórios de erros. Como agora, quando a propriedade de um recurso está sendo referenciada (e, portanto, compartilhada na propriedade) entre, digamos, 8 partes diferentes do sistema, é preciso apenas uma falha para remover o recurso em resposta ao usuário solicitando sua remoção. vazar e possivelmente indefinidamente.

Portanto, nunca fui um grande fã do GC ou da contagem de referência aplicada em larga escala devido à facilidade com que eles criaram software com vazamento. O que anteriormente teria sido um travamento de ponteiro oscilante, fácil de detectar, se transforma em um vazamento de recursos muito difícil de detectar, que pode voar facilmente sob o radar dos testes.

Referências fracas podem atenuar esse problema se a linguagem / biblioteca as fornecer, mas achei difícil contratar uma equipe de desenvolvedores de conjuntos de habilidades mistas para poder usar consistentemente referências fracas sempre que apropriado. E essa dificuldade não estava relacionada apenas à equipe interna, mas a todos os desenvolvedores de plug-ins do nosso software. Eles também poderiam facilmente fazer com que o sistema vazasse recursos, apenas armazenando uma referência persistente a um objeto de maneiras que dificultavam o rastreamento do plug-in como o culpado; portanto, também obtivemos a maior parte dos relatórios de erros resultantes dos recursos do software. sendo vazado simplesmente porque um plug-in cujo código fonte estava fora do nosso controle falhou ao liberar referências a esses recursos caros.

Solução: Remoção Adiada Periódica

Então, minha solução, mais tarde, na qual apliquei meus projetos pessoais que me deram o melhor que encontrei nos dois mundos, foi eliminar o conceito de que, referencing=ownershipainda assim, adiamos a destruição de recursos.

Como resultado, agora, sempre que o usuário faz algo que faz com que um recurso precise ser removido, a API é expressa em termos de apenas remover o recurso:

ecs->remove(component);

... que modela a lógica do usuário final de maneira muito direta. No entanto, o recurso (componente) pode não ser removido imediatamente se houver outros encadeamentos do sistema em sua fase de processamento em que eles possam acessar o mesmo componente simultaneamente.

Portanto, esses encadeamentos de processamento produzem tempo aqui e ali, o que permite que um encadeamento semelhante a um coletor de lixo seja ativado e " pare o mundo " e destrua todos os recursos que foram solicitados a serem removidos enquanto impedia que os encadeamentos processassem esses componentes até sua conclusão. . Eu ajustei isso para que a quantidade de trabalho que precise ser feita aqui seja geralmente mínima e não corte visivelmente as taxas de quadros.

Agora não posso dizer que este é um método testado e bem documentado, mas é algo que venho usando há alguns anos, sem dores de cabeça e sem vazamentos de recursos. Eu recomendo explorar abordagens como essa quando for possível para sua arquitetura se encaixar nesse tipo de modelo de simultaneidade, pois é muito menos trabalhoso do que GC ou ref-counting e não corre o risco desses tipos de vazamentos de recursos passarem pelo radar dos testes.

O único lugar em que achei útil a ref-counting ou GC é para estruturas de dados persistentes. Nesse caso, é o território da estrutura de dados, muito distanciado das preocupações dos usuários, e ali faz sentido que cada cópia imutável esteja compartilhando a propriedade dos mesmos dados não modificados.


fonte