Olá galera,
O Singleton é um padrão super popular e muito se deve à sua facilidade de implementação. Entretanto, as implementações que muitos conhecemos e vemos por aí podem ter falhas, principalmente no que diz respeito a sincronismo de Threads. Dependendo do contexto, isso pode se tornar perigoso e quando menos esperamos, o erro está em um Singleton mal implementado. Esse tipo de falha é difícil de identificar em código legado, e aí o padrão simples custa caro.
Vou mostrar algumas formas diferentes de implementar o Singleton, cada uma com seus ganhos e perdas, e como o Rafael Tolotti costuma dizer, “tudo é um trade”. Algumas considerações:
- Usei sealed em todas as classes para otimizar o JIT, reduzindo custo no dispatching de métodos virtuais (vide artigo sobre Virtual Method Tables e sobre Sealed Classes).
- Mesmo sem decorar as classes com sealed é importante deixarmos os membros de instância e classe como private. Evita que o princípio do padrão seja quebrado em caso de herança entre classes (sub-classes poderiam instanciar a base-class).
1) Versão “non-thread safe“
namespace Singleton.Sample.Implementations { /// <summary> /// PS: Keyword "sealed" is used for JIT optimizations /// </summary> public sealed class Singleton_1_NonThreadSafe { private static Singleton_1_NonThreadSafe instance = null; private Singleton_1_NonThreadSafe() { } public static Singleton_1_NonThreadSafe Instance { get { if (instance == null) { instance = new Singleton_1_NonThreadSafe(); } return instance; } } } }
Comentários: Essa é a versão mais simples e popular do Singleton. Entretanto, se duas ou mais Threads caíssem na linha 16 poderíamos ter uma quebra do princípio e várias instâncias seriam criadas. Abordagem recomendada em cenários simplíssimos, onde por prerrogativa não haverá concorrência no acesso à instância Singleton.
2) Versão “Simple Thread-Safe”
namespace Singleton.Sample.Implementations { /// <summary> /// PS: Keyword "sealed" is used for JIT optimizations /// PS 2: This class uses a critical zone to solve the concurrent memory-barrier issue. /// </summary> public sealed class Singleton_2_SimpleThreadSafe { private static Singleton_2_SimpleThreadSafe instance = null; private static readonly object syncRoot = new object(); Singleton_2_SimpleThreadSafe() { } public static Singleton_2_SimpleThreadSafe Instance { get { lock (syncRoot) { if (instance == null) { instance = new Singleton_2_SimpleThreadSafe(); } return instance; } } } } }
Comentários: Versão simples para evitar que duas ou mais threads avaliem a expressão da linha 20 e instanciem o objeto. Para isso é utilizado um objeto imutável de sincronização (cadeado) (criado na linha 10 e trancado na linha 18). Pode apresentar problemas de performance pois um lock é gerenciado toda vez que a instância for acessada.
Obs: Existe uma variação para esta versão que evita que toda vez o lock seja acessado, usando uma condição adicional antes mesmo do lock. Porém ela se torna mais problemática ainda por apresentar brechas em relação ao modelo de gerência de memória do Framework e não sendo tão performática quanto outras implementações.
3) Versão Thread-Safe com Static Constructors e sem cadeados
namespace Singleton.Sample.Implementations { /// <summary> /// Static constructor tells the compiler to lazy-load the type-initialization (by NOT marking the class with beforeFieldInit) /// and runs only ONCE per AppDomain (what makes it Thread-safe). /// </summary> public sealed class Singleton_3_ThreadSafeWithoutLock { private static readonly Singleton_3_ThreadSafeWithoutLock instance = new Singleton_3_ThreadSafeWithoutLock(); static Singleton_3_ThreadSafeWithoutLock() { } private Singleton_3_ThreadSafeWithoutLock() { } public Singleton_3_ThreadSafeWithoutLock Instance { get { return instance; } } } }
Comentários: Assim como comentei na classe, o segredo está no Static Constructor. Ano passado eu fiz um post sobre Static Constructors, sugiro que seja lido para melhor entendimento. O Static Constructor vai garantir que a inicialização dos tipos (nesta versão é in-line, de acordo com [09]) seja executada apenas uma vez por AppDomain, e também que as variáveis sejam inicializadas quando o primeiro membro estático da classe for acessado, o que de certa forma, garante o fake-lazy-load (digo “fake” porque se nenhum membro estático for acessado os type-initializers não executarão). Além do ganho de performance com o lazy-loading, o código fica bem “clean” e a menos que queiramos fazer um eager-loading (inicializando a instância dentro do Static Constructor), o Static Constructor deverá ficar vazio.
Clique aqui para baixar a solução de código usada no post.
Galera por hoje é isso vou tentar não sumir tanto do blog hehe.
Abraço!