¿Puedo declarar el predicado dispatch_once_t como una variable miembro en lugar de estática?

Quiero ejecutar un bloque de código solo una vez por instancia.

¿Puedo declarar el predicado dispatch_once_t como una variable miembro en lugar de una variable estática?

Desde GCD Reference , no está claro para mí.

El predicado debe apuntar a una variable almacenada en el scope global o estático. El resultado de utilizar un predicado con almacenamiento automático o dynamic no está definido.

Sé que puedo usar dispatch_semaphore_t y un indicador booleano para hacer lo mismo. Tengo curiosidad.

dispatch_once_t no debe ser una variable de instancia.

La implementación de dispatch_once() requiere que dispatch_once_t sea ​​cero y nunca haya sido distinto de cero . El caso anteriormente distinto de cero necesitaría barreras de memoria adicionales para funcionar correctamente, pero dispatch_once() omite esas barreras por razones de rendimiento.

Las variables de instancia se inicializan a cero, pero su memoria puede haber almacenado previamente otro valor. Esto los hace inseguros para el uso de dispatch_once() .

Actualización 16 de noviembre

Esta pregunta fue respondida originalmente en 2012 con una “diversión”, no pretendía proporcionar una respuesta definitiva y tenía una advertencia al respecto. En retrospectiva, tal entretenimiento probablemente debería haber sido privado, aunque algunos lo disfrutaron.

En agosto de 2016, esta sesión de preguntas y respuestas me llamó la atención y proporcioné una respuesta adecuada. En eso escribió:

Aparentemente no estoy de acuerdo con Greg Parker, pero probablemente no realmente …

Bueno, parece que Greg y yo no estamos de acuerdo sobre si estamos en desacuerdo, o la respuesta, o algo así 😉 Así que estoy actualizando mi respuesta de agosto de 2016 con una base más detallada para la respuesta, por qué podría estar mal, y en caso afirmativo cómo arreglarlo (por lo que la respuesta a la pregunta original sigue siendo “sí”). Espero que Greg y yo estemos de acuerdo o aprenda algo, ¡o el resultado es bueno!

Entonces, primero la respuesta del 16 de agosto como estaba, luego una explicación de la base de la respuesta. La diversión original se ha eliminado para evitar confusiones, los estudiantes de historia pueden ver el recorrido de edición.


Respuesta: agosto de 2016

Aparentemente no estoy de acuerdo con Greg Parker, pero probablemente no realmente …

La pregunta original:

¿Puedo declarar el predicado dispatch_once_t como una variable miembro en lugar de una variable estática?

Respuesta corta: la respuesta es sí PROPORCIONADO que hay una barrera de memoria entre la creación inicial del objeto y cualquier uso de dispatch_once .

Explicación rápida: El requisito de la variable dispatch_once para dispatch_once es que inicialmente debe ser cero. Lo difícil proviene de las operaciones de reordenamiento de memoria en los multiprocesadores modernos. Si bien puede parecer que una tienda en una ubicación se ha realizado de acuerdo con el texto del progtwig (lenguaje de alto nivel o nivel de ensamblador) la tienda real puede reordenarse y ocurrir después de una lectura posterior de la misma ubicación. Para abordar este problema, se pueden utilizar barreras de memoria que fuerzan todas las operaciones de memoria que se producen antes de que se completen antes que las que las siguen. Apple proporciona el OSMemoryBarrier() para hacer esto.

Con dispatch_once Apple afirma que se garantiza que las variables globales inicializadas cero sean cero, pero que las variables de instancia inicializadas en cero (y la inicialización cero es el valor predeterminado de Objective-C aquí) no se garantiza que sean cero antes de que se ejecute un dispatch_once .

La solución es insertar una barrera de memoria; en el supuesto de que el dispatch_once ocurre en algún método miembro de una instancia, el lugar obvio para poner esta barrera de memoria está en el método init , ya que (1) solo se ejecutará una vez (por instancia) y (2) init debe haber regresado antes se puede llamar a cualquier otro método miembro.

Entonces sí, con una barrera de memoria apropiada, dispatch_once se puede usar con una variable de instancia.


Nov 2016

Preámbulo: Notas sobre dispatch_once

Estas notas se basan en el código de Apple y en los comentarios de dispatch_once .

El uso de dispatch_once sigue el patrón estándar:

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

y las últimas dos líneas se expanden en línea ( dispatch_once es una macro) a algo así como:

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

Notas:

  • La fuente de Apple establece que el predicate debe inicializarse a cero y observa que las variables globales y estáticas se inicializan por defecto a cero.

  • Tenga en cuenta que en la línea [A] no hay barrera de memoria. En un procesador con lectura anticipada especulativa y predicción de bifurcación, la lectura de cachedValue en la línea [C] podría ocurrir antes de la lectura del predicate en la línea [A], lo que podría generar resultados incorrectos (un valor incorrecto para cachedValue )

  • Se podría usar una barrera para evitar esto, sin embargo, eso es lento y Apple quiere que esto sea rápido en el caso común de que el bloque una vez se haya realizado, así que …

  • dispatch_once_internal , línea [B], que usa barreras y operaciones atómicas internamente, usa una barrera especial, dispatch_atomic_maximally_synchronizing_barrier() para vencer la lectura anticipada especulativa y permitir que la línea [A] esté libre de barreras y, por lo tanto, sea rápida.

  • Cualquier procesador que llegue a la línea [A] antes de dispatch_once_internal() se haya ejecutado y el predicate mutado necesite leer 0 desde el predicate . Usar un global o estático inicializado a cero para el predicate lo garantizará.

Lo importante para nuestros propósitos actuales es que dispatch_once_internal mutates predicate de tal manera que la línea [A] funcione sin ninguna barrera.

Larga explicación del 16 de agosto Respuesta:

Por lo tanto, sabemos que el uso de un sistema global o estático inicializado a cero cumple los requisitos del camino rápido sin barreras de dispatch_once() . También sabemos que las mutaciones hechas por dispatch_once_internal() al predicate se manejan correctamente.

Lo que necesitamos determinar es si podemos usar una variable de instancia para el predicate e inicializarla de tal manera que la línea [A] anterior nunca pueda leer su valor preinicializado, como si pudiera romperse.

Mi respuesta del 16 de agosto dice que esto es posible. Para comprender la base de esto, debemos considerar el flujo de datos y el progtwig en un entorno multiprocesador con lectura anticipada especulativa.

El resumen de la ejecución de la respuesta y el flujo de datos del 16 de agosto es:

 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 una variable de instancia como el predicado, entonces debe ser imposible ejecutar el paso 9 de tal forma que lea el valor en la memoria antes de que el paso 1 lo haya puesto a cero.

Si se omite el paso 4, es decir, no se inserta ninguna barrera apropiada en init , aunque el procesador 2 debe obtener el valor correcto para la referencia de objeto generada por el procesador 1 antes de poder ejecutar el paso 9, es (teóricamente) posible que el procesador 1 sea cero las escrituras en el paso 1 aún no se han realizado / escrito en la memoria global y el procesador 2 no las verá.

Entonces, insertamos el paso 4 y realizamos una barrera.

Sin embargo, ahora tenemos que considerar la lectura anticipada especulativa, al igual que dispatch_once() tiene que hacerlo. ¿Podría el procesador 2 realizar la lectura del paso 9 antes de que la barrera del paso 4 haya asegurado que la memoria es cero?

Considerar:

  • El procesador 2 no puede realizar, especulativamente o de otro modo, la lectura del paso 9 hasta que tenga la referencia de objeto obtenida en el paso 7, y para hacerlo especulativamente requiere que el procesador determine que la llamada al método en el paso 8, cuyo destino en Objective-C es dinámicamente determinado, terminará en el método que contiene el paso 9, que es una especulación bastante avanzada (pero no imposible);

  • El paso 7 no puede obtener la referencia del objeto hasta que el paso 6 lo haya almacenado / pasado;

  • El paso 6 no lo tiene para almacenar / pasar hasta que el paso 5 lo haya devuelto; y

  • El paso 5 es después de la barrera en el paso 4 …

TL; DR : ¿Cómo puede el paso 9 tener la referencia de objeto requerida para realizar la lectura hasta después del paso 4 que contiene la barrera? (Y dado el largo camino de ejecución, con múltiples twigs, algunas condicional (por ejemplo, el envío interno de métodos), ¿es un problema de lectura anticipada un problema?)

Por lo tanto, sostengo que la barrera en el paso 4 es suficiente, incluso en presencia de lectura anticipada especulativa que efectúa el paso 9.

Consideración de los comentarios de Greg:

Greg reforzó el comentario del código fuente de Apple con respecto al predicado de “debe inicializarse a cero” a “nunca debe haber sido distinto de cero”, lo que significa que el tiempo de carga, y esto solo es cierto para las variables globales y estáticas inicializadas a cero. El argumento se basa en la derrota de la lectura anticipada especulativa por los procesadores modernos requeridos para la ruta rápida dispatch_once() sin barreras.

Las variables de instancia se inicializan a cero en el momento de creación del objeto, y la memoria que ocupan podría haber sido distinta de cero antes de eso. Sin embargo, como se ha argumentado anteriormente, se puede usar una barrera adecuada para asegurar que dispatch_once() no lea un valor de pre-inicialización. Creo que Greg no está de acuerdo con mi argumento, si sigo sus comentarios correctamente, y argumenta que la barrera en el paso 4 es insuficiente para manejar la lectura anticipada especulativa.

Supongamos que Greg tiene razón (¡lo cual no es del todo improbable!), Entonces estamos en una situación que Apple ya ha solucionado en dispatch_once() , necesitamos vencer a la lectura anticipada. Apple lo hace utilizando la barrera dispatch_atomic_maximally_synchronizing_barrier() . Podemos usar esta misma barrera en el paso 4 e impedir que se ejecute el siguiente código hasta que se haya superado toda posible lectura especulativa por delante del Procesador 2; y como el siguiente código, los pasos 5 y 6, deben ejecutarse antes de que el procesador 2 tenga una referencia de objeto que pueda usar para realizar especulativamente el paso 9, todo funciona.

Entonces, si entiendo las preocupaciones de Greg, usar dispatch_atomic_maximally_synchronizing_barrier() resolverá, y usarlas en lugar de una barrera estándar no causará problemas, incluso si no son realmente necesarias. Así que aunque no estoy convencido de que sea necesario , en el peor de los casos es inofensivo hacerlo. Por lo tanto, mi conclusión sigue siendo la misma que antes (énfasis añadido):

Entonces sí, con una barrera de memoria apropiada , dispatch_once se puede usar con una variable de instancia.

Estoy seguro de que Greg u otro lector me avisarán si he cometido un error en mi lógica. ¡Estoy listo para enfrentar la palmada!

Por supuesto, usted tiene que decidir si el costo de la barrera apropiada en init vale el beneficio que obtiene de usar dispatch_once() para obtener el comportamiento de una vez por instancia o si debe abordar sus requisitos de otra manera, y esas alternativas están fuera del scope. scope de esta respuesta!

Código para dispatch_atomic_maximally_synchronizing_barrier() :

Una definición de dispatch_atomic_maximally_synchronizing_barrier() , adaptada de la fuente de Apple, que puede usar en su propio código es:

 #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 

Si quieres saber cómo funciona, lee el código fuente de Apple.

La referencia que cite parece bastante clara: el predicado tiene que estar en un scope global o estático, si lo usa como una variable miembro, será dynamic, por lo que el resultado será indefinido. Entonces no, no puedes. dispatch_once() no es lo que está buscando (la referencia también dice: Ejecuta un objeto de bloque una vez y solo durante la vida de una aplicación , que no es lo que quiere ya que quiere que este bloque se ejecute para cada instancia).