Aquisição de recursos é inicialização - Resource acquisition is initialization

Aquisição de recursos é inicialização ( RAII ) é um idioma de programação usado em várias linguagens de programação orientadas a objetos e estaticamente tipadas para descrever o comportamento de uma linguagem específica. Em RAII, mantendo um recurso é uma invariante classe , e está ligada a vida útil do objeto : atribuição de recursos (ou aquisição) é feito durante a criação do objecto (especificamente de inicialização), pelo construtor , enquanto desatribuição recurso (libertação) é feito durante a destruição objecto ( especificamente finalização), pelo destruidor . Em outras palavras, a aquisição de recursos deve ser bem-sucedida para que a inicialização seja bem-sucedida. Portanto, é garantido que o recurso seja mantido entre o término da inicialização e o início da finalização (manter os recursos é uma invariante de classe) e mantido apenas quando o objeto estiver ativo. Portanto, se não houver vazamentos de objetos, não haverá vazamentos de recursos .

RAII está associado mais proeminentemente com C ++ onde se originou, mas também D , Ada , Vala e Rust . A técnica foi desenvolvida para gerenciamento de recursos seguro de exceção em C ++ durante 1984–89, principalmente por Bjarne Stroustrup e Andrew Koenig , e o próprio termo foi cunhado por Stroustrup. RAII é geralmente pronunciado como um initialism , às vezes pronunciado como "R, A, double I".

Outros nomes para esse idioma incluem Aquisições de construtor, Liberações de destruidor (CADRe) e um estilo particular de uso é chamado Gerenciamento de recursos baseado em escopo (SBRM). Este último termo é para o caso especial de variáveis ​​automáticas . O RAII vincula recursos ao tempo de vida do objeto , que pode não coincidir com a entrada e a saída de um escopo. (Notavelmente, as variáveis ​​alocadas no armazenamento gratuito têm tempos de vida não relacionados a qualquer escopo determinado.) No entanto, o uso de RAII para variáveis ​​automáticas (SBRM) é o caso de uso mais comum.

Exemplo C ++ 11

O exemplo C ++ 11 a seguir demonstra o uso de RAII para acesso a arquivos e bloqueio mutex :

#include <fstream>
#include <iostream>
#include <mutex>
#include <stdexcept>
#include <string>

void WriteToFile(const std::string& message) {
  // |mutex| is to protect access to |file| (which is shared across threads).
  static std::mutex mutex;

  // Lock |mutex| before accessing |file|.
  std::lock_guard<std::mutex> lock(mutex);

  // Try to open file.
  std::ofstream file("example.txt");
  if (!file.is_open()) {
    throw std::runtime_error("unable to open file");
  }

  // Write |message| to |file|.
  file << message << std::endl;

  // |file| will be closed first when leaving scope (regardless of exception)
  // |mutex| will be unlocked second (from lock destructor) when leaving scope
  // (regardless of exception).
}

Esse código é seguro para exceções porque o C ++ garante que todos os objetos da pilha sejam destruídos no final do escopo delimitador, conhecido como desenrolamento da pilha . Os destruidores de ambos os objetos de bloqueio e arquivo são, portanto, garantidos para serem chamados ao retornar da função, independentemente de uma exceção ter sido lançada ou não.

Variáveis ​​locais permitem fácil gerenciamento de vários recursos dentro de uma única função: elas são destruídas na ordem reversa de sua construção, e um objeto é destruído apenas se totalmente construído - isto é, se nenhuma exceção se propagar de seu construtor.

O uso de RAII simplifica muito o gerenciamento de recursos, reduz o tamanho geral do código e ajuda a garantir a correção do programa. RAII é, portanto, recomendado pelas diretrizes padrão da indústria, e a maior parte da biblioteca padrão C ++ segue o idioma.

Benefícios

As vantagens do RAII como técnica de gerenciamento de recursos são que ele fornece encapsulamento, segurança de exceção (para recursos de pilha) e localidade (permite que a aquisição e a lógica de liberação sejam gravadas lado a lado).

O encapsulamento é fornecido porque a lógica de gerenciamento de recursos é definida uma vez na classe, não em cada local de chamada. Segurança de exceção é fornecida para recursos de pilha (recursos que são liberados no mesmo escopo em que são adquiridos), vinculando o recurso ao tempo de vida de uma variável de pilha (uma variável local declarada em um determinado escopo): se uma exceção for lançada, e o tratamento adequado de exceções está em vigor, o único código que será executado ao sair do escopo atual são os destruidores de objetos declarados nesse escopo. Finalmente, a localidade da definição é fornecida escrevendo as definições do construtor e do destruidor um ao lado do outro na definição da classe.

O gerenciamento de recursos, portanto, precisa estar vinculado à vida útil de objetos adequados para obter alocação e recuperação automáticas. Os recursos são adquiridos durante a inicialização, quando não há chance de serem utilizados antes de serem disponibilizados, e liberados com a destruição dos mesmos objetos, o que é garantido mesmo em caso de erros.

Comparando RAII com a finallyconstrução usada em Java, Stroustrup escreveu que “Em sistemas realistas, há muito mais aquisições de recursos do que tipos de recursos, então a técnica 'aquisição de recursos é inicialização' leva a menos código do que o uso de uma construção 'finalmente'. ”

Usos típicos

O design RAII é freqüentemente usado para controlar bloqueios mutex em aplicativos multi-threaded . Nesse uso, o objeto libera o bloqueio quando destruído. Sem o RAII neste cenário, o potencial de deadlock seria alto e a lógica para bloquear o mutex estaria longe da lógica para desbloqueá-lo. Com RAII, o código que bloqueia o mutex inclui essencialmente a lógica de que o bloqueio será liberado quando a execução deixar o escopo do objeto RAII.

Outro exemplo típico é a interação com arquivos: Poderíamos ter um objeto que representa um arquivo que está aberto para escrita, onde o arquivo é aberto no construtor e fechado quando a execução sai do escopo do objeto. Em ambos os casos, o RAII garante apenas que o recurso em questão seja liberado de forma adequada; ainda deve ser tomado cuidado para manter a segurança da exceção. Se o código que modifica a estrutura de dados ou o arquivo não for seguro para exceções, o mutex pode ser desbloqueado ou o arquivo fechado com a estrutura de dados ou arquivo corrompido.

A propriedade de objetos alocados dinamicamente (memória alocada com newem C ++) também pode ser controlada com RAII, de forma que o objeto seja liberado quando o objeto RAII (baseado em pilha) for destruído. Para este propósito, a biblioteca padrão C ++ 11 define as classes de ponteiro inteligentestd::unique_ptr para objetos de propriedade única e std::shared_ptrpara objetos com propriedade compartilhada. Classes semelhantes também estão disponíveis através std::auto_ptrde C ++ 98, e boost::shared_ptrnas bibliotecas de impulso .

Extensões de "limpeza" do compilador

Ambos Clang e o GNU Compiler Collection implementar uma extensão não-padrão para o C linguagem para RAII apoio: a "limpeza" atributo variável. A macro a seguir anota uma variável com uma determinada função destruidora que chamará quando a variável sair do escopo:

static inline void fclosep(FILE **fp) { if (*fp) fclose(*fp); }
#define _cleanup_fclose_ __attribute__((cleanup(fclosep)))

Essa macro pode ser usada da seguinte maneira:

void example_usage() {
  _cleanup_fclose_ FILE *logfile = fopen("logfile.txt", "w+");
  fputs("hello logfile!", logfile);
}

Neste exemplo, o compilador faz com que a função fclosep seja chamada no arquivo de log antes do retorno de example_usage .

Limitações

RAII só funciona para recursos adquiridos e liberados (direta ou indiretamente) por objetos alocados-stack, onde não é uma vida objeto estático bem definido. Objetos alocados por heap que adquirem e liberam recursos são comuns em muitas linguagens, incluindo C ++. RAII depende de objetos baseados em heap para serem excluídos implícita ou explicitamente ao longo de todos os caminhos de execução possíveis, a fim de acionar seu destruidor de liberação de recursos (ou equivalente). Isso pode ser alcançado usando ponteiros inteligentes para gerenciar todos os objetos de heap, com ponteiros fracos para objetos referenciados ciclicamente.

Em C ++, o desenrolamento da pilha só tem garantia de ocorrer se a exceção for detectada em algum lugar. Isso ocorre porque "Se nenhum manipulador correspondente for encontrado em um programa, a função terminate () é chamada; se a pilha é ou não desfeita antes que esta chamada para terminate () seja definida pela implementação (15.5.1)." (Padrão C ++ 03, §15.3 / 9). Este comportamento é geralmente aceitável, uma vez que o sistema operacional libera os recursos restantes, como memória, arquivos, soquetes, etc. no encerramento do programa.

Contagem de referência

Perl , Python (na implementação CPython ) e PHP gerenciam o tempo de vida do objeto por contagem de referência , o que torna possível usar RAII. Objetos que não são mais referenciados são imediatamente destruídos ou finalizados e liberados, portanto, um destruidor ou finalizador pode liberar o recurso naquele momento. No entanto, nem sempre é idiomático em tais linguagens e é especificamente desencorajado em Python (em favor de gerenciadores de contexto e finalizadores do pacote weakref ).

No entanto, os tempos de vida dos objetos não são necessariamente limitados a qualquer escopo e os objetos podem ser destruídos de forma não determinística ou não podem ser destruídos. Isso possibilita o vazamento acidental de recursos que deveriam ter sido liberados no final de algum escopo. Objetos armazenados em uma variável estática (notavelmente uma variável global ) podem não ser finalizados quando o programa termina, então seus recursos não são liberados; CPython não garante a finalização de tais objetos, por exemplo. Além disso, objetos com referências circulares não serão coletados por um contador de referência simples e viverão por tempo indeterminado; mesmo se coletado (por uma coleta de lixo mais sofisticada), o tempo de destruição e a ordem de destruição serão não determinísticos. No CPython existe um detector de ciclo que detecta ciclos e finaliza os objetos no ciclo, embora antes do CPython 3.4, os ciclos não sejam coletados se algum objeto no ciclo tiver um finalizador.

Referências

Leitura adicional

links externos