Fundamentos para Performance

Lidando com recursos gerenciados e não gerenciados em .NET

Podemos classificar os recursos utizados em .NET em duas categorias: gerenciados e não gerenciados.

Recursos gerenciados são desalocados automaticamente. Por outro lado, recursos não gerenciados dependem de uma estratégia adequada de desalocação explícita, geralmente implementada através do Dispose Pattern.

IMPORTANTE: É comum, entre os programadores .NET, associar do método Dispose()com liberação de memória. Entretanto, é importante destacar que, frequentemente, este não é o caso. Como iremos demonstrar, recursos não gerenciados podem ser de outros tipos.

O que são recursos gerenciados?

Quando falamos de recursos gerenciados estamos tratando, na prática, da memória gerenciada.

A memória gerenciada é aquela associada a objetos na heap que, quando não tem mais nenhuma referênica no código, são coletados, eventualmente, pelo Garbage Collector. Também estamos falando dos valores armazenados na stack que são desalocados quando há um descarte de um stack frame.

O que são recursos não gerenciados

Recursos não gerenciados são todos os recursos alocados pela aplicação, exceto a memória gerenciada.

Estão entre os recursos não gerenciados a memória alocada que não é controlada pelo Garbage Collector, como aquela alocada em rotinas escritas em C++. Também são recursos não gerenciados os handles de arquivos, sockets e outros recursos de rede, conexões com banco de dados, objetos GDI, etc.

Recursos não gerenciados geralmente são liberados através do método Dispose.

O método Dispose

Objetos que utilizam recursos não gerenciados geralmente implementam alguma estratégia de desalocação explícita desses objetos, geralmente através do método Dispose.

É importante lembrar que objetos não gerenciados não são desalocados automaticamente em .NET. Se um objeto implementa o método Dispose, então, este deve ser evocado explicitamente no código (ou implicitamente, através da utilização da instrução using).

Objetos que implementam o método Dispose, definido na interface IDisposable, deveriam implementar o Dispose Pattern.

Dispose Pattern

Dispose Pattern existe para garantir que objetos que utilizam recursos não gerenciados irão os descartar quando forem coletados pelo GC, mesmo que não tenham tido sua implementação para o método Dispose executada.

Abaixo, podemos ver um exemplo de implementação recomendada do dispose pattern proposta pelo Visual Studio:

O que o código relacionado ao Dispose Pattern faz?

Quando implementamos o Dispose Pattern, no método Dispose() executado sem parâmetros, chamamos o método Dispose(true) que, realizará as operações de liberação de recursos não gerenciados e também fará uma chamada ao método SuppressFinalize() do garbage collector, impedindo que este execute o finalizador para o objeto quando ele for coletado.

Se o programador esquecer de chamar o método Dispose explicitamente, então, eventualmente, o GC irá executar o finalizador e executará o Dispose(false).

NOTA: O método Finalize (finalizador) é executado pelo Garbage Collector sempre antes de descartar um objeto da memória gerenciada. Por isso, é uma boa ideia chamar o método Dispose no finalizador para garantir que recursos gerenciados serão liberados, mesmo quando o programador esquecer de chamar o método Dispose.

Cuidados ao definir código para um finalizador

Como indicamos, os finalizadores permitem definir código que será executado quando uma instância é descartada pelo GC. Entretanto, é bom lembrar que o GC opera de maneira não determinística, não sendo possível determinar quando o código do finalizador será, finalmente, executado. Assim, não podemos determinar, também, quando o recurso não gerenciado será finalmente liberado (no caso do programador cliente “esquecer” de executar o Dispose explicitamente).

Outro aspecto importante é que, por objetivar performance, o GC não descarta objetos, em Gen #0, que possuam finalizadores promovendo-os imediatamente para Gen #1. O que aumenta o custo do descarte.

NOTA: O método SuppressFinalize() “avisa” o GC que não é necessário executar o código do finalizador antes da coleta autorizando seu descarte em Gen #0,

Concluindo

É muito importante que saibamos identificar rapidamente recursos gerenciados e recursos não gerenciados. Recursos não gerenciados frequentemente não tem relação direta com memória alocada.

Sempre que utilizamos recursos não gerenciados, temos que projetar o descarte desses, preferencialmente de forma explícita pois ela não ocorrerá automaticamente.

Se um tipo contiver entre seus atributos um recurso não gerenciado, então deverá implementar a interface IDisposable.

A implementação de IDisposable respeitando o Dispose Pattern garante que recursos não gerenciados sejam sempre descartados.

Objetos que implementam finalizadores são mais custosos para liberar. Por isso, devemos lembrar de chamar o método Dispose explicitamente sempre.

Mais posts da série Fundamentos para Performance

4 Comentários
  1. Rogério dos Santos Fernandes

    Bem bom o POST! Curti!!

  2. Antonio Maniero

    Só lembrando que *ref struct* em C# 8 permite ter um método *Dispose()*, sem implementar explicitamente a interface *IDisposable*, o que é uma espécie de *duck typing* da linguagem (não disponível para o programador criar seu próprio). E neste caso não envolve GC porque este tipo só pode ser alocado na *stack*.

  3. Júlio Costa

    Olá, tudo bem?
    Achei muito interessante o post, mas fiquei com uma dúvida, quanto ao texto abaixo:

    NOTA: O método Finalize (finalizador) é executado pelo Garbage Collector sempre antes de descartar um objeto da memória gerenciada.

    Quem é esse método Finalize(finalizador)?
    Quando vc diz finalizador, está se referindo ao destrutor da classe que tem a chamada para Dispose(false)?

    Muito obrigado

    1. Elemar Júnior

      Em C# não há destrutores. O método que “parece” o destrutor é, na prática, o Finalizer

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *