Posso declarar o predicado dispatch_once_t como uma variável de membro em vez de estático?

Eu quero executar um bloco de código apenas uma vez por instância.

Posso declarar o predicado dispatch_once_t como uma variável de membro em vez de uma variável estática?

Do GCD Reference , não está claro para mim.

O predicado deve apontar para uma variável armazenada no escopo global ou estático. O resultado do uso de um predicado com armazenamento automático ou dynamic é indefinido.

Eu sei que posso usar dispatch_semaphore_t e uma bandeira booleana para fazer a mesma coisa. Eu só estou curioso.

dispatch_once_t não deve ser uma variável de instância.

A implementação de dispatch_once() requer que o dispatch_once_t seja zero e nunca tenha sido diferente de zero . O caso anterior, que não é zero, precisaria de barreiras de memory adicionais para funcionar corretamente, mas dispatch_once() omite essas barreiras por motivos de desempenho.

Variáveis ​​de instância são inicializadas para zero, mas sua memory pode ter armazenado anteriormente outro valor. Isso os torna inseguros para uso em dispatch_once() .

Atualização 16 de novembro

Esta questão foi originalmente respondida em 2012 com uma “diversão”, não pretendeu fornecer uma resposta definitiva e carregou uma ressalva para esse efeito. Em retrospectiva, tal divertimento provavelmente deveria ter sido privado, embora alguns gostassem.

Em agosto de 2016, esta session de perguntas e respostas foi trazida à minha atenção e forneci uma resposta adequada. Naquele escreveu:

Eu vou aparentemente discordar de Greg Parker, mas provavelmente não realmente …

Bem, parece que Greg e eu discordamos sobre se discordamos, ou a resposta, ou algo assim 😉 Então estou atualizando a minha resposta de agosto de 2016 com uma base mais detalhada para a resposta, por que isso pode estar errado, e se assim for para corrigi-lo (então a resposta para a pergunta original ainda é “sim”). Espero que Greg e eu concordemos, ou eu vou aprender alguma coisa – ou o resultado é bom!

Então, primeiro, a resposta de 16 de agosto foi, então, uma explicação da base da resposta. A diversão original foi removida para evitar qualquer confusão, os alunos do histórico podem ver a trilha de edição.


Resposta: ago 2016

Eu vou aparentemente discordar de Greg Parker, mas provavelmente não realmente …

A pergunta original:

Posso declarar o predicado dispatch_once_t como uma variável de membro em vez de uma variável estática?

Resposta curta: A resposta é sim FORNECIDA existe uma barreira de memory entre a criação inicial do object e qualquer uso de dispatch_once .

Explicação rápida: O requisito da variável dispatch_once para dispatch_once é que ela deve ser inicialmente zero. A dificuldade vem das operações de reordenamento de memory em multiprocessadores modernos. Embora possa parecer que uma loja para um local foi executada de acordo com o texto do programa (linguagem de alto nível ou nível assembler), o armazenamento real pode ser reordenado e ocorrer após uma leitura subsequente do mesmo local. Para endereçar esta memory, podem ser usadas barreiras que forcem todas as operações de memory que ocorrem antes delas para serem concluídas antes daqueles que as seguem. A Apple fornece o OSMemoryBarrier() para fazer isso.

Com dispatch_once Apple está declarando que as variables ​​globais inicializadas com zero são garantidas como zero, mas que as variables ​​de instância inicializadas com zero (e zero inicializando é o padrão Objective-C aqui) não são garantidas como zero antes que um dispatch_once seja executado.

A solução é inserir uma barreira de memory; supondo que o dispatch_once ocorra em algum método de membro de uma instância, o local óbvio para colocar essa barreira de memory é no método init como (1) ele será executado somente uma vez (por instância) e (2) init deve ter retornado antes qualquer outro método membro pode ser chamado.

Então sim, com uma barreira de memory apropriada, dispatch_once pode ser usado com uma variável de instância.


Nov 2016

Preâmbulo: Notas sobre dispatch_once

Estas notas são baseadas no código da Apple e comentários para dispatch_once .

O uso de dispatch_once segue o padrão padrão:

 id cachedValue; dispatch_once_t predicate = 0; ... dispatch_once(&predicate, ^{ cachedValue = expensiveComputation(); }); ... use cachedValue ... 

e as duas últimas linhas são expandidas em linha ( dispatch_once é uma macro) para algo como:

 if (predicate != ~0) // (all 1's, indicates the block has been executed) [A] { dispatch_once_internal(&predicate, block); // [B] } ... use cachedValue ... // [C] 

Notas:

  • A fonte da Apple afirma que o predicate deve ser inicializado para zero e observa que as variables ​​globais e estáticas assumem o padrão de boot zero.

  • Note que na linha [A] não há barreira de memory. Em um processador com predicação especulativa de leitura e ramificação, a leitura de cachedValue na linha [C] poderia ocorrer antes da leitura do predicate na linha [A], o que poderia levar a resultados incorretos (um valor incorreto para cachedValue )

  • Uma barreira pode ser usada para evitar isso, no entanto, isso é lento e a Apple quer que isso seja rápido no caso comum de que o bloco já tenha sido executado, então …

  • dispatch_once_internal , linha [B], que usa barreiras e operações atômicas internamente, usa uma barreira especial, dispatch_atomic_maximally_synchronizing_barrier() para derrotar a leitura especulativa e assim permitir que a linha [A] esteja livre de barreiras e, portanto, rápida.

  • Qualquer processador que atinge a linha [A] antes de dispatch_once_internal() ter sido executado e o predicate mutado precisa ler 0 no predicate . Usar um global ou estático inicializado como zero para predicate garantirá isso.

O importante para nossos propósitos atuais é que dispatch_once_internal modifica o predicate de tal maneira que a linha [A] funciona sem nenhuma barreira.

Long Explicação de 16 de agosto Resposta:

Portanto, sabemos que usar um global ou estático inicializado em zero atende aos requisitos do atalho sem barreira do dispatch_once() . Também sabemos que as mutações feitas por dispatch_once_internal() para o predicate são tratadas corretamente.

O que precisamos determinar é se podemos usar uma variável de instância para o predicate e inicializá-la de tal maneira que a linha [A] acima nunca possa ler seu valor pré-inicializado – como se isso pudesse quebrar.

Minha resposta de 16 de agosto diz que isso é possível. Para entender a base para isso, precisamos considerar o stream de dados e programas em um ambiente multiprocessador com leitura antecipada especulativa.

O esboço da execução e do stream de dados da resposta de 16 de agosto é:

 Processor 1 Processor 2 0. Call alloc 1. Zero instance var used for predicate 2. Return object ref from alloc 3. Call init passing object ref 4. Perform barrier 5. Return object ref from init 6. Store or send object ref somewhere ... 7. Obtain object ref 8. Call instance method passing obj ref 9. In called instance method dispatch_once tests predicate, This read is dependent on passed obj ref. 

Para poder usar uma variável de instância como predicado, deve ser impossível executar a etapa 9 de tal forma que leia o valor na memory antes que a etapa 1 tenha zerado.

Se a etapa 4 for omitida, ou seja, nenhuma barreira apropriada for inserida no init , embora o Processador 2 deva obter o valor correto para a referência de object gerada pelo Processador 1 antes de poder executar a etapa 9, é (teoricamente) possível que o Processador 1 seja zero as escritas na etapa 1 ainda não foram executadas / gravadas na memory global e o Processador 2 não as verá.

Então, inserimos o passo 4 e fazemos uma barreira.

No entanto, agora temos que considerar a leitura especulativa, assim como dispatch_once() tem que fazer. O Processador 2 poderia executar a leitura da etapa 9 antes que a barreira da etapa 4 tenha assegurado que a memory seja zero?

Considerar:

  • O processador 2 não pode realizar, especulativamente ou de outra forma, a leitura da etapa 9 até que tenha a referência de object obtida na etapa 7 – e para fazê-lo especulativamente requer que o processador determine que a chamada de método na etapa 8, cujo destino em Objective-C é determinado dinamicamente, terminará no método que contém a etapa 9, que é especulação bastante avançada (mas não impossível);

  • O passo 7 não pode obter a referência do object até que o passo 6 tenha armazenado / passado;

  • O passo 6 não conseguiu armazenar / passar até o passo 5 ter devolvido; e

  • O passo 5 é depois da barreira no passo 4 …

TL; DR : Como a etapa 9 pode ter a referência de object necessária para executar a leitura até depois da etapa 4 que contém a barreira? (E dado o longo caminho de execução, com múltiplas ramificações, algumas condicionais (por exemplo, dentro do método de envio), a leitura especulativa é um problema?)

Por isso, argumento que a barreira no passo 4 é suficiente, mesmo na presença do passo 9 de efeito de leitura especulativa.

Consideração dos comentários de Greg:

Greg reforçou o comentário do código fonte da Apple sobre o predicado de “deve ser inicializado para zero” para “nunca deve ter sido diferente de zero”, o que significa desde o tempo de carregamento, e isso é verdade apenas para variables ​​globais e estáticas inicializadas em zero. O argumento é baseado em derrotar a leitura especulativa pelos processadores modernos necessários para o caminho rápido dispatch_once() sem barreiras.

As variables ​​de instância são inicializadas para zero no momento da criação do object, e a memory que elas ocupam poderia ter sido diferente de zero antes disso. No entanto, como foi argumentado acima, uma barreira adequada pode ser usada para garantir que dispatch_once() não leia um valor de pré-boot. Eu acho que Greg discorda do meu argumento, se eu seguir seus comentários corretamente, e argumenta que a barreira no passo 4 é insuficiente para lidar com a leitura especulativa.

Vamos supor que Greg está certo (o que não é de todo improvável!), Então estamos em uma situação que a Apple já lidou com dispatch_once() , precisamos derrotar a leitura antecipada. A Apple faz isso usando a barreira dispatch_atomic_maximally_synchronizing_barrier() . Podemos usar essa mesma barreira na etapa 4 e impedir que o código a seguir seja executado até que toda a possível leitura especulativa à frente pelo Processador 2 tenha sido derrotada; e como o seguinte código, as etapas 5 e 6, devem ser executados antes que o Processador 2 tenha até uma referência de object que possa usar para executar especulativamente a etapa 9, tudo funciona.

Então, se eu entendi as preocupações de Greg, então usar o dispatch_atomic_maximally_synchronizing_barrier() irá resolvê-los, e usá-lo em vez de uma barreira padrão não causará um problema, mesmo que não seja realmente necessário. Então, embora eu não esteja convencido de que seja necessário , é no máximo inofensivo fazê-lo. Minha conclusão, portanto, permanece como antes (grifo nosso):

Então sim, com uma barreira de memory apropriada , dispatch_once pode ser usado com uma variável de instância.

Tenho certeza de que Greg ou algum outro leitor me avisará se eu tiver errado na minha lógica. Eu estou pronto para facepalm!

É claro que você tem que decidir se o custo da barreira apropriada no init vale o benefício que você ganha ao usar dispatch_once() para obter o comportamento uma vez por instância ou se deve abordar seus requisitos de outra maneira – e essas alternativas estão fora do alcance desta resposta!

Código para dispatch_atomic_maximally_synchronizing_barrier() :

Uma definição de dispatch_atomic_maximally_synchronizing_barrier() , adaptada da fonte da Apple, que você pode usar em seu próprio código é:

 #if defined(__x86_64__) || defined(__i386__) #define dispatch_atomic_maximally_synchronizing_barrier() \ ({ unsigned long _clbr; __asm__ __volatile__( "cpuid" : "=a" (_clbr) : "0" (0) : "ebx", "ecx", "edx", "cc", "memory"); }) #else #define dispatch_atomic_maximally_synchronizing_barrier() \ ({ __c11_atomic_thread_fence(dispatch_atomic_memory_order_seq_cst); }) #endif 

Se você quiser saber como isso funciona, leia o código-fonte da Apple.

A referência que você cita parece bastante clara: o predicado tem que estar no escopo global ou estático, se você usá-lo como uma variável membro, ele será dynamic, então o resultado será indefinido. Então não, você não pode. dispatch_once() não é o que você está procurando (a referência também diz: Executa um object de bloco uma vez e somente uma vez para o tempo de vida de um aplicativo , que não é o que você quer desde que você queira que esse bloco seja executado para cada instância).