Por que a Apple recomenda usar o dispatch_once para implementar o padrão singleton no ARC?

305

Qual é o motivo exato do uso de dispatch_once no acessador de instância compartilhada de um singleton no ARC?

+ (MyClass *)sharedInstance
{
    //  Static local predicate must be initialized to 0
    static MyClass *sharedInstance = nil;
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[MyClass alloc] init];
        // Do any other initialisation stuff here
    });
    return sharedInstance;
}

Não é uma má idéia instanciar o singleton de forma assíncrona em segundo plano? Quero dizer, o que acontece se eu solicitar essa instância compartilhada e confiar nela imediatamente, mas o dispatch_once leva até o Natal para criar meu objeto? Não retorna imediatamente, certo? Pelo menos, esse parece ser o objetivo principal do Grand Central Dispatch.

Então, por que eles estão fazendo isso?

Membro orgulhoso
fonte
Note: static and global variables default to zero.
ikkentim

Respostas:

418

dispatch_once()é absolutamente síncrono. Nem todos os métodos GCD fazem as coisas de forma assíncrona (caso em questão, dispatch_sync()é síncrona). O uso de dispatch_once()substitui o seguinte idioma:

+ (MyClass *)sharedInstance {
    static MyClass *sharedInstance;
    @synchronized(self) {
        if (sharedInstance == nil) {
            sharedInstance = [[MyClass alloc] init];
        }
    }
    return sharedInstance;
}

O benefício dispatch_once()disso é que é mais rápido. Também é semanticamente mais limpo, porque também protege você de vários threads executando alocação init de sharedInstance - se todos tentarem ao mesmo tempo. Não permitirá que duas instâncias sejam criadas. A idéia toda dispatch_once()é "executar algo de uma vez e apenas uma vez", que é precisamente o que estamos fazendo.

Lily Ballard
fonte
4
Para o argumento, preciso observar que a documentação não diz que está sendo executada de forma síncrona. Diz apenas que várias invocações simultâneas serão serializadas.
expert
5
Você alega que é mais rápido - quanto mais rápido? Não tenho motivos para pensar que você não está dizendo a verdade, mas gostaria de ver uma referência simples.
Joshua Gross
29
Acabei de fazer uma referência simples (no iPhone 5) e parece que dispatch_once é cerca de duas vezes mais rápido que o @synchronized.
Joshua Gross #
3
@ReneDohan: Se você tem 100% de certeza de que ninguém nunca chama esse método a partir de um thread diferente, ele funciona. Mas o uso dispatch_once()é realmente simples (especialmente porque o Xcode o completa automaticamente em um snippet de código completo para você) e significa que você nunca precisa considerar se o método precisa ser seguro para threads.
Lily Ballard
1
@siuying: Na verdade, isso não é verdade. Primeiro, tudo o que é feito +initializeacontece antes que a classe seja tocada, mesmo que você ainda não esteja tentando criar sua instância compartilhada. Em geral, a inicialização lenta (criando algo apenas quando necessário) é melhor. Segundo, mesmo sua reivindicação de desempenho não é verdadeira. dispatch_once()leva quase exatamente a mesma quantidade de tempo como dizendo if (self == [MyClass class])na +initialize. Se você já possui um +initialize, sim, criar a instância compartilhada é mais rápido, mas a maioria das classes não.
Lily Ballard
41

Porque ele será executado apenas uma vez. Portanto, se você tentar acessá-lo duas vezes a partir de threads diferentes, isso não causará problemas.

Mike Ash tem uma descrição completa em seu post no blog Care and Feeding of Singletons .

Nem todos os blocos GCD são executados de forma assíncrona.

Abizern
fonte
A de Lily é uma resposta melhor, mas estou deixando a minha para manter o link para o post de Mike Ash.
Abizern 18/03/19