Bloqueo de doble verificación

La versión actual de la página aún no ha sido revisada por colaboradores experimentados y puede diferir significativamente de la versión revisada el 20 de septiembre de 2017; las comprobaciones requieren 7 ediciones .
Bloqueo de doble verificación
Bloqueo doble comprobado
Descrito en Patrones de diseño No

El bloqueo de doble verificación es un  patrón de diseño paralelo diseñado para reducir los gastos generales asociados con la obtención de un bloqueo. Primero, la condición de bloqueo se verifica sin ninguna sincronización; el subproceso intenta adquirir el bloqueo solo si el resultado de la comprobación indica que necesita adquirir el bloqueo.

En algunos lenguajes y/o en algunas máquinas no es posible implementar este patrón de manera segura. Por lo tanto, a veces se le llama antipatrón . Tales características han llevado a la relación de orden estricto " sucede antes " en el modelo de memoria de Java y el modelo de memoria de C++.

Se usa comúnmente para reducir la sobrecarga de implementar la inicialización diferida en programas de subprocesos múltiples, como parte del patrón de diseño Singleton . Con la inicialización diferida de una variable, la inicialización se retrasa hasta que se necesita el valor de la variable en el cálculo.

Ejemplo de uso de Java

Considere el siguiente código Java tomado de [1] :

// Clase de versión de subproceso único Foo { ayudante privado helper = null ; ayudante público getHelper () { if ( ayudante == nulo ) ayudante = nuevo ayudante (); ayudante de retorno ; } // y otros miembros de la clase... }

Este código no funcionará correctamente en un programa de subprocesos múltiples. El método getHelper()debe adquirir un bloqueo en caso de que se llame simultáneamente desde dos subprocesos. De hecho, si el campo helperaún no se ha inicializado y dos subprocesos llaman al método al mismo tiempo getHelper(), ambos subprocesos intentarán crear un objeto, lo que conducirá a la creación de un objeto adicional. Este problema se resuelve utilizando la sincronización, como se muestra en el siguiente ejemplo.

// Correcta, pero "cara" versión de subprocesos múltiples class Foo { private Helper helper = null ; Helper público sincronizado getHelper () { if ( helper == null ) helper = new Helper (); ayudante de retorno ; } // y otros miembros de la clase... }

Este código funciona, pero presenta una sobrecarga de sincronización adicional. La primera llamada getHelper()creará el objeto, y solo es getHelper()necesario sincronizar los pocos subprocesos que se llamarán durante la inicialización del objeto. Una vez inicializada, la sincronización en llamada getHelper()es redundante ya que solo leerá la variable. Dado que la sincronización puede reducir el rendimiento en un factor de 100 o más, la sobrecarga de bloqueo cada vez que se llama a este método parece innecesaria: una vez que se completa la inicialización, ya no se necesita el bloqueo. Muchos programadores han intentado optimizar este código de esta manera:

  1. Primero, verifica si la variable está inicializada (sin obtener un bloqueo). Si se inicializa, su valor se devuelve inmediatamente.
  2. Conseguir un candado.
  3. Comprueba de nuevo si la variable está inicializada, ya que es muy posible que después de la primera comprobación, otro subproceso haya inicializado la variable. Si se inicializa, se devuelve su valor.
  4. De lo contrario, la variable se inicializa y se devuelve.
// Versión de subprocesos múltiples incorrecta (en Symantec JIT y Java versiones 1.4 y anteriores) // Patrón de "Bloqueo de verificación doble" class Foo { private Helper helper = null ; ayudante público getHelper () { si ( ayudante == nulo ) { sincronizado ( esto ) { si ( ayudante == nulo ) { ayudante = nuevo Ayudante (); } } } ayudante de retorno ; } // y otros miembros de la clase... }

En un nivel intuitivo, este código parece correcto. Sin embargo, hay algunos problemas (en Java 1.4 y anteriores e implementaciones de JRE no estándar) que tal vez deberían evitarse. Imagine que los eventos en un programa de subprocesos múltiples proceden así:

  1. El subproceso A nota que la variable no está inicializada, luego adquiere el bloqueo y comienza la inicialización.
  2. Semántica de algunos lenguajes de programación.[ ¿Qué? ] es tal que el subproceso A puede asignar una referencia a un objeto que está en proceso de inicialización a una variable compartida (lo que, en general, claramente viola la relación causal, porque el programador claramente solicitó asignar una referencia a un objeto a la variable [es decir, publicar una referencia en compartido] - en el momento posterior a la inicialización, y no en el momento anterior a la inicialización).
  3. El subproceso B nota que la variable está inicializada (al menos eso cree) y devuelve el valor de la variable sin adquirir un bloqueo. Si el subproceso B ahora usa la variable antes de que el subproceso A haya terminado de inicializarse, el comportamiento del programa será incorrecto.

Uno de los peligros de usar el bloqueo de verificación doble en J2SE 1.4 (y versiones anteriores) es que el programa a menudo parece funcionar correctamente. Primero, la situación considerada no ocurrirá muy a menudo; en segundo lugar, es difícil distinguir la correcta implementación de este patrón de la que presenta el problema descrito. Dependiendo del compilador , la asignación de tiempo de procesador a los subprocesos por parte del programador y la naturaleza de otros procesos simultáneos en ejecución, los errores causados ​​por la implementación incorrecta del bloqueo de verificación doble generalmente ocurren al azar. La reproducción de tales errores suele ser difícil.

Puede resolver el problema utilizando J2SE 5.0 . La nueva semántica de palabras clave volatilehace posible manejar correctamente la escritura en una variable en este caso. Este nuevo patrón se describe en [1] :

// Funciona con la nueva semántica volátil // No funciona en Java 1.4 y versiones anteriores debido a la semántica volátil class Foo { private volatile Helper helper = null ; ayudante público getHelper () { si ( ayudante == nulo ) { sincronizado ( esto ) { si ( ayudante == nulo ) ayudante = nuevo Ayudante (); } } ayudante de retorno ; } // y otros miembros de la clase... }

Se han propuesto muchas opciones de bloqueo comprobadas que no indican explícitamente (a través de la volatilidad o la sincronización) que un objeto está completamente construido, y todas ellas son incorrectas para Symantec JIT y Oracle JRE heredado [2] [3] .

Ejemplo de uso en C#

singleton de clase sellada pública { singleton privado () { // inicializa una nueva instancia de objeto } singletonInstance de Singleton volátil estático privado ; Private static readonly Object syncRoot = new Object (); public static Singleton GetInstance () { // se ha creado el objeto if ( singletonInstance == null ) { // no, no se ha creado // solo un subproceso puede crearlo lock ( syncRoot ) { // comprueba si otro subproceso ha creado el object if ( singletonInstance == null ) { // no, no lo creó - create singletonInstance = new Singleton (); } } } return instancia única ; } }

Microsoft confirma [4] que cuando se usa la palabra clave volatile, es seguro usar el patrón de bloqueo Doublechecked.

Un ejemplo de uso en Python

El siguiente código de Python muestra un ejemplo de implementación de la inicialización diferida en combinación con el patrón de bloqueo de verificación doble:

# requiere Python2 o Python3 #-*- codificación: UTF-8 *-* importar hilos clase SimpleLazyProxy : '''inicialización de objeto perezoso a salvo de amenazas''' def __init__ ( auto , fábrica ): auto . __bloquear = enhebrar . RLock () auto . __obj = Ninguno propio . __fábrica = fábrica def __call__ ( self ): '''función para acceder al objeto real si el objeto no se crea, se creará ''' # intente obtener acceso "rápido" al objeto: obj = self . __obj si obj no es Ninguno : # ¡logrado! return obj else : # es posible que el objeto aún no se haya creado con uno mismo __lock : # obtener acceso al objeto en modo exclusivo: obj = self . __obj si obj no es Ninguno : # resulta que el objeto ya ha sido creado. # no lo recrees return obj else : # el objeto aún no se ha creado realmente. # ¡vamos a crearlo! obj = uno mismo . __fábrica () uno mismo __obj = obj devolver objeto __getattr__ = lambda self , nombre : \ getattr ( self (), nombre ) def lazy ( proxy_cls = SimpleLazyProxy ): '''decorador que convierte una clase en una clase con inicialización diferida mediante la clase Proxy'''' class ClassDecorator : def __init__ ( self , cls ): # inicialización del decorador, # pero no la clase que se está decorando ni la clase proxy uno mismo cls = cls def __call__ ( self , * args , ** kwargs ): # llamada para la inicialización de la clase proxy # pasar los parámetros necesarios a la clase Proxy # para inicializar la clase que se está decorando devuelve proxy_cls ( lambda : self . cls ( * args , ** kwargs )) devolver ClassDecorator # comprobación sencilla: def test_0 (): print ( ' \t\t\t *** Inicio de prueba ***' ) tiempo de importación @lazy () # instancias de esta clase serán clase de inicialización diferida TestType : def __init__ ( self , name ): print ( ' %s : Created...' % name ) # aumentar artificialmente el tiempo de creación de objetos # para aumentar la competencia de subprocesos tiempo _ dormir ( 3 ) uno mismo nombre = nombre print ( ' %s : ¡Creado!' % nombre ) def test ( self ): print ( ' %s : Probando' % self . nombre ) # una instancia de este tipo interactuará con varios subprocesos test_obj = TestType ( 'Objeto de prueba entre subprocesos' ) target_event = enhebrado . Evento () def threads_target (): # función que ejecutarán los hilos: # espera un evento especial target_event . espera () # tan pronto como ocurra este evento - # los 10 subprocesos accederán simultáneamente al objeto de prueba # y en este momento se inicializará en uno de los subprocesos test_obj . prueba () # crea estos 10 subprocesos con el algoritmo anterior threads_target() threads = [] for thread in range ( 10 ): thread = threading . Hilo ( objetivo = hilo_objetivo ) hilo _ iniciar () subprocesos . adjuntar ( hilo ) print ( 'No ha habido accesos al objeto hasta ahora' ) # espera un poco... tiempo . dormir ( 3 ) # ...y ejecuta test_obj.test() simultáneamente en todos los subprocesos print ( '¡Dispara el evento para usar el objeto de prueba!' ) target_event . conjunto () # fin de hilo en hilos : hilo . unirse () imprimir ( ' \t\t\t *** Fin de la prueba ***' )

Enlaces

  • Bloqueo doble comprobado y patrón Singleton
  • https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom es el mejor reemplazo para este patrón
  • DobleBloqueoComprobado  . _ — Una descripción de este patrón en Java y temas relacionados. Archivado desde el original el 1 de marzo de 2012.
  •  Implementando Singleton en C# . - Artículo de MSDN (Singleton, Threadsafe Singleton). Archivado desde el original el 1 de marzo de 2012.

Notas

  1. David Bacon, Joshua Bloch y otros. La declaración "Bloqueo doblemente verificado está roto" . Sitio web de Bill Pugh. Archivado desde el original el 1 de marzo de 2012.