Implementação do Padrão Singleton em C++ e Gestão de Concorrência

O padrão de projeto Singleton assegura que uma classe tenha apenas uma instância e fornece um ponto de acesso global a ela. Essa abordagem é particularmente útil quando um único objeto é requerido para coordenar operações em todo o sistema, ou quando a instância única precisa ser extensível através de subclasses, permitindo que os clientes utilizem a versão estendida sem alterações no código-fonte.

A responsabilidade principal recai sobre a própria classe, que deve encapsular a criação de sua instância e controlar o acesso a ela, geralmente através de um método estático. O construtor é deifnido como protegido ou privado para impedir a instanciação externa direta.

Implementação com Inicialização Tardia

A inicialização tardia (lazy initialization) posterga a criação do objeto até o momento em que ele é requisitado pela primeira vez. No exemplo abaixo, utilizamos um gerador de terrenos para um motor de jogos para ilustrar essa abordagem:

class TerrainGenerator {
public:
    static TerrainGenerator* getInstance();
protected:
    TerrainGenerator() = default;
private:
    static TerrainGenerator* uniqueInstance;
};

TerrainGenerator* TerrainGenerator::uniqueInstance = nullptr;

TerrainGenerator* TerrainGenerator::getInstance() {
    if (uniqueInstance == nullptr) {
        uniqueInstance = new TerrainGenerator();
    }
    return uniqueInstance;
}

Em cenários onde o tipo exato da instância depende de configurações externas, podemos expandir a lógica de criação para instanciar subclasses específicas baseadas em variáveis de ambiente:

#include <cstdlib>
#include <string>

TerrainGenerator* TerrainGenerator::getInstance() {
    if (uniqueInstance == nullptr) {
        const char* terrainType = std::getenv("TERRAIN_TYPE");
        std::string envConfig = terrainType ? terrainType : "";
        
        if (envConfig == "volcanic") {
            uniqueInstance = new VolcanicTerrainGenerator();
        } else if (envConfig == "arctic") {
            uniqueInstance = new ArcticTerrainGenerator();
        } else {
            uniqueInstance = new TerrainGenerator();
        }
    }
    return uniqueInstance;
}

Concorrência e Segurança de Threads

A implementação tardia básica apresenta uma falha crítica em ambientes multithread: múltiplas threads podem avaliar a condição nula simultaneamente, resultando na criação de múltiplas instâncias. Para mitigar isso, empregamos a técnica de bloqueio de dupla verificação (Double-Checked Locking) com mutexes:

#include <mutex>

class TerrainGenerator {
public:
    static TerrainGenerator* getInstance();
protected:
    TerrainGenerator() = default;
private:
    static TerrainGenerator* uniqueInstance;
    static std::mutex instantiationMutex;
};

TerrainGenerator* TerrainGenerator::uniqueInstance = nullptr;
std::mutex TerrainGenerator::instantiationMutex;

TerrainGenerator* TerrainGenerator::getInstance() {
    if (uniqueInstance == nullptr) {
        std::lock_guard<std::mutex> lock(instantiationMutex);
        if (uniqueInstance == nullptr) {
            uniqueInstance = new TerrainGenerator();
        }
    }
    return uniqueInstance;
}

Inicialização Antecipada

Alternativamente, a inicialização antecipada (eager initialization) cria a instância no momento da carga da classe, antes da execução da função principal. Como a inicialização de variáveis estáticas globais é gerenciada pelo thread principal antes de qualquer concorrência, essa abordagem é intrinsecamente segura contra condições de corrida, eliminando a sobrecarga de bloqueios em tempo de execução:

TerrainGenerator* TerrainGenerator::uniqueInstance = new TerrainGenerator();

TerrainGenerator* TerrainGenerator::getInstance() {
    return uniqueInstance;
}

Tags: singleton design-patterns cpp Concurrency thread-safety

Publicado em 9-23 19:48