Padrão de Projeto Singleton em C#

O padrão Singleton garante que uma classe tenha apenas uma instância e fornece um ponto global de acesso a essa instância. Este padrão é particularmente útil quando precisamos controlar o acesso a recursos compartilhados como conexões de banco de dados ou arquivos de configuração.

Implementação Básica

using System;

UnicoServico.ObterInstancia().ExibirIdentificador();
UnicoServico.ObterInstancia().ExibirIdentificador();
UnicoServico.ObterInstancia().ExibirIdentificador();

class UnicoServico
{
    private Guid _identificador;
    private static UnicoServico _instancia;
    
    private UnicoServico()
    {
        _identificador = Guid.NewGuid();
    }
    
    public static UnicoServico ObterInstancia()
    {
        if (_instancia == null)
        {
            _instancia = new UnicoServico();
        }
        return _instancia;
    }
    
    public void ExibirIdentificador() => Console.WriteLine(_identificador);
}

Problemas em Ambientes Concorrentes

A implementação básica não é segura para ambientes com múltiplos threads. Em cenários concorrentes, múltiplas instâncias podem ser criadas:

using System.Threading.Tasks;

// Em ambientes concorrentes, não garantimos que sempre será a mesma instância
Parallel.For(0, 3, indice =>
{
    var id = UnicoServico.ObterInstancia().ObterIdentificador();
    Console.WriteLine($"Execução {indice}: {id}");
});

class UnicoServico
{
    private Guid _identificador;
    private static UnicoServico _instancia;
    
    private UnicoServico()
    {
        _identificador = Guid.NewGuid();
    }
    
    public static UnicoServico ObterInstancia()
    {
        if (_instancia == null)
        {
            _instancia = new UnicoServico();
        }
        return _instancia;
    }
    
    public Guid ObterIdentificador() => _identificador;
}

Saída típica em ambiente concorretne:

Execução 1: 1624c990-26e5-4c73-add1-fc3b736c1059
Execução 0: 7782f74d-9100-4115-9d37-c5a2cd40f481
Execução 2: 2ab01b9a-48c3-45af-b613-ced3def060b5

Implementação com Controle de Concorrência

Para resolver problemas de concorrência, podemos implementar um mecanismo de bloqueio:

using System.Threading.Tasks;

// Com mecanismo de bloqueio, garantimos a mesma instância mesmo em ambientes concorrentes
Parallel.For(0, 30, indice =>
{
    var id = UnicoServico.ObterInstancia().ObterIdentificador();
    Console.WriteLine($"Execução {indice}: {id}");
});

class UnicoServico
{
    private Guid _identificador;
    private static readonly object _bloqueio = new object();
    private static UnicoServico _instancia;
    
    private UnicoServico()
    {
        _identificador = Guid.NewGuid();
    }
    
    public static UnicoServico ObterInstancia()
    {
        if (_instancia == null)
        {
            lock (_bloqueio)
            {
                if (_instancia == null)
                {
                    _instancia = new UnicoServico();
                }
            }
        }
        return _instancia;
    }
    
    public Guid ObterIdentificador() => _identificador;
}

Implementação com Double-Check Locking

Para melhorar o desempenho, podemos usar o padrão Double-Check Locking:

class UnicoServico
{
    private Guid _identificador;
    private static readonly object _bloqueio = new object();
    private static UnicoServico _instancia;
    
    private UnicoServico()
    {
        _identificador = Guid.NewGuid();
    }
    
    public static UnicoServico ObterInstancia()
    {
        if (_instancia == null)
        {
            lock (_bloqueio)
            {
                if (_instancia == null)
                {
                    _instancia = new UnicoServico();
                }
            }
        }
        return _instancia;
    }
    
    public Guid ObterIdentificador() => _identificador;
}

Altenrativa Moderna com Lazy Initialization

No .NET 4.0 e posteriores, podemos usar a classe Lazy<T> para uma implementação mais simples e eficeinte:

class UnicoServico
{
    private Guid _identificador;
    private static readonly Lazy<UnicoServico> _instanciaLazy = 
        new Lazy<UnicoServico>(() => new UnicoServico());
    
    private UnicoServico()
    {
        _identificador = Guid.NewGuid();
    }
    
    public static UnicoServico ObterInstancia() => _instanciaLazy.Value;
    
    public Guid ObterIdentificador() => _identificador;
}

Esta abordagem é thread-safe por padrão e oferece inicialização preguiçosa (lazy initialization) com alto desempenho.

Tags: singleton design-patterns C# thread-safety concurrent-programming

Publicado em 6-10 17:03 por Thomas