En ingeniería de software , el bloqueo de doble verificación (también conocido como "optimización de bloqueo de doble verificación" [1] ) es un patrón de diseño de software que se utiliza para reducir la sobrecarga de adquirir un bloqueo probando el criterio de bloqueo (la "sugerencia de bloqueo") antes adquirir la cerradura. El bloqueo se produce solo si la comprobación del criterio de bloqueo indica que se requiere bloqueo.
El patrón, cuando se implementa en algunas combinaciones de lenguaje / hardware, puede ser inseguro. A veces, puede considerarse un antipatrón . [2]
Por lo general, se usa para reducir la sobrecarga de bloqueo al implementar una " inicialización diferida " en un entorno de subprocesos múltiples, especialmente como parte del patrón Singleton . La inicialización diferida evita inicializar un valor hasta la primera vez que se accede a él.
Uso en C ++ 11
Para el patrón singleton, no se necesita un bloqueo con doble verificación:
Si el control ingresa a la declaración al mismo tiempo mientras se inicializa la variable, la ejecución concurrente esperará a que se complete la inicialización.
- § 6.7 [stmt.dcl] p4
Singleton y GetInstance () { static Singleton s ; return s ; }
C ++ 11 y posteriores también proporcionan un patrón de bloqueo de doble verificación integrado en forma de std::once_flag
y std::call_once
:
#include #include // Desde C ++ 17// Singleton.h class Singleton { public : static Singleton * GetInstance (); privado : Singleton () = predeterminado ; std estático :: opcional < Singleton > s_instance ; static std :: once_flag s_flag ; };// Singleton.cpp std :: opcional < Singleton > Singleton :: s_instance ; std :: once_flag Singleton :: s_flag {};Singleton * Singleton :: GetInstance () { std :: call_once ( Singleton :: s_flag , [] () { s_instance . Emplace ( Singleton {}); }); return & * s_instance ; }
Si uno realmente desea usar el idioma verificado dos veces en lugar del ejemplo trivialmente funcional anterior (por ejemplo, porque Visual Studio antes del lanzamiento de 2015 no implementó el lenguaje del estándar C ++ 11 sobre la inicialización concurrente citado anteriormente [3] ), uno necesita para utilizar adquirir y liberar vallas: [4]
#include ómico>#include class Singleton { público : Singleton estático * GetInstance (); privado : Singleton () = predeterminado ; std estático :: atomic < Singleton *> s_instance ; static std :: mutex s_mutex ; };Singleton * Singleton :: GetInstance () { Singleton * p = s_instance . cargar ( std :: memory_order_acquire ); if ( p == nullptr ) { // 1er cheque std :: lock_guard < std :: mutex > lock ( s_mutex ); p = s_instancia . cargar ( std :: memory_order_relaxed ); if ( p == nullptr ) { // 2nd (doble) check p = new Singleton (); s_instancia . store ( p , std :: memory_order_release ); } } return p ; }
Uso en Go
paquete principalimportar "sincronizar"var arrOnce sync . Once var arr [] int// getArr recupera arr, inicializándose perezosamente en la primera llamada. El bloqueo // verificado dos veces se implementa con la función de biblioteca sync.Once. La primera // goroutine en ganar la carrera para llamar a Do () inicializará la matriz, mientras que // otras se bloquearán hasta que Do () se haya completado. Después de que se haya ejecutado Do, solo se requerirá // una única comparación atómica para obtener la matriz. func getArr () [] int { arrOnce . Do ( func () { arr = [] int { 0 , 1 , 2 } }) return arr }func main () { // gracias al bloqueo verificado dos veces, dos goroutines que intentan getArr () // no causarán una inicialización doble go getArr () go getArr () }
Uso en Java
Considere, por ejemplo, este segmento de código en el lenguaje de programación Java dado por [2] (así como todos los demás segmentos de código Java):
// Clase de versión de un solo subproceso Foo { ayudante auxiliar estático privado ; public Helper getHelper () { if ( helper == null ) { helper = new Helper (); } return helper ; } // otras funciones y miembros ... }
El problema es que esto no funciona cuando se utilizan varios subprocesos. Se debe obtener un bloqueo en caso de que dos hilos llamen getHelper()
simultáneamente. De lo contrario, ambos pueden intentar crear el objeto al mismo tiempo, o uno puede terminar obteniendo una referencia a un objeto inicializado de forma incompleta.
El bloqueo se obtiene mediante una costosa sincronización, como se muestra en el siguiente ejemplo.
// Versión multiproceso correcta pero posiblemente costosa class Foo { Private Helper helper ; pública sincronizado ayudante getHelper () { si ( ayudante == nula ) { ayudante = nueva ayudante (); } return helper ; } // otras funciones y miembros ... }
Sin embargo, la primera llamada a getHelper()
creará el objeto y solo los pocos subprocesos que intentan acceder a él durante ese tiempo deben sincronizarse; después de eso, todas las llamadas solo obtienen una referencia a la variable miembro. Dado que la sincronización de un método podría, en algunos casos extremos, disminuir el rendimiento en un factor de 100 o más, [5] la sobrecarga de adquirir y liberar un bloqueo cada vez que se llama a este método parece innecesario: una vez que se ha completado la inicialización, adquirir y liberar el las cerraduras parecerían innecesarias. Muchos programadores han intentado optimizar esta situación de la siguiente manera:
- Compruebe que la variable esté inicializada (sin obtener el bloqueo). Si está inicializado, devuélvalo inmediatamente.
- Obtén el candado.
- Verifique si la variable ya se ha inicializado: si otro hilo adquirió el bloqueo primero, es posible que ya haya realizado la inicialización. Si es así, devuelva la variable inicializada.
- De lo contrario, inicialice y devuelva la variable.
// Versión multiproceso rota // Clase idiomática "Bloqueo con doble verificación" Foo { Private Helper helper ; public Helper getHelper () { if ( helper == null ) { sincronizado ( esto ) { if ( helper == null ) { helper = new Helper (); } } } return helper ; } // otras funciones y miembros ... }
Intuitivamente, este algoritmo parece una solución eficiente al problema. Sin embargo, esta técnica tiene muchos problemas sutiles y, por lo general, debe evitarse. Por ejemplo, considere la siguiente secuencia de eventos:
- El subproceso A advierte que el valor no está inicializado, por lo que obtiene el bloqueo y comienza a inicializar el valor.
- Debido a la semántica de algunos lenguajes de programación, el código generado por el compilador puede actualizar la variable compartida para apuntar a un objeto parcialmente construido antes de que A haya terminado de realizar la inicialización. Por ejemplo, en Java, si se ha insertado una llamada a un constructor, la variable compartida puede actualizarse inmediatamente una vez que se haya asignado el almacenamiento, pero antes de que el constructor integrado inicialice el objeto. [6]
- El subproceso B advierte que la variable compartida se ha inicializado (o eso parece) y devuelve su valor. Debido a que el subproceso B cree que el valor ya está inicializado, no adquiere el bloqueo. Si B usa el objeto antes de que B vea toda la inicialización realizada por A (ya sea porque A no ha terminado de inicializarlo o porque algunos de los valores inicializados en el objeto aún no se han filtrado a la memoria que B usa ( coherencia de caché )) , es probable que el programa se bloquee.
Uno de los peligros de usar el bloqueo doble verificado en J2SE 1.4 (y versiones anteriores) es que a menudo parecerá que funciona: no es fácil distinguir entre una implementación correcta de la técnica y una que tiene problemas sutiles. Dependiendo del compilador , el entrelazado de subprocesos por parte del programador y la naturaleza de otra actividad concurrente del sistema , las fallas resultantes de una implementación incorrecta del bloqueo doble verificado solo pueden ocurrir de manera intermitente. Reproducir los fallos puede resultar complicado.
A partir de J2SE 5.0 , este problema se ha solucionado. La palabra clave volátil ahora garantiza que varios subprocesos manejen correctamente la instancia de singleton. Este nuevo idioma se describe en [3] y [4] .
// Funciona con la semántica de adquisición / liberación para volátiles en Java 1.5 y posterior // Roto bajo Java 1.4 y semántica anterior para la clase volátil Foo { ayudante auxiliar volátil privado ; public Helper getHelper () { Helper localRef = helper ; if ( localRef == null ) { sincronizado ( esto ) { localRef = helper ; if ( localRef == null ) { helper = localRef = new Helper (); } } } return localRef ; } // otras funciones y miembros ... }
Tenga en cuenta la variable local " localRef ", que parece innecesaria. El efecto de esto es que en los casos en que helper ya está inicializado (es decir, la mayor parte del tiempo), solo se accede al campo volátil una vez (debido a " return localRef; " en vez de " ayudante de retorno; "), que puede mejorar el rendimiento general del método hasta en un 40 por ciento. [7]
Java 9 introdujo la VarHandle
clase, que permite el uso de átomos relajados para acceder a los campos, dando lecturas algo más rápidas en máquinas con modelos de memoria débiles, a costa de mecánicas más difíciles y pérdida de consistencia secuencial (los accesos de campo ya no participan en el orden de sincronización, el orden global de accesos a campos volátiles). [8]
// Funciona con la semántica de adquisición / liberación para VarHandles introducida en Java 9 class Foo { asistente auxiliar volátil privado ; public Helper getHelper () { Helper localRef = getHelperAcquire (); if ( localRef == null ) { sincronizado ( esto ) { localRef = getHelperAcquire (); if ( localRef == null ) { localRef = new Helper (); setHelperRelease ( localRef ); } } } return localRef ; } AYUDANTE de VarHandle final estático privado ; Ayudante privado getHelperAcquire () { return ( Ayudante ) AYUDANTE . getAcquire ( esto ); } private void setHelperRelease ( valor de ayuda ) { HELPER . setRelease ( este , valor ); } static { try { MethodHandles . Búsqueda de búsqueda = MethodHandles . buscar (); AYUDANTE = buscar . findVarHandle ( Foo . clase , "ayudante" , ayudante . clase ); } catch ( ReflectiveOperationException e ) { lanzar nuevo ExceptionInInitializerError ( e ); } } // otras funciones y miembros ... }
Si el objeto auxiliar es estático (uno por cargador de clases), una alternativa es el modismo de titular de inicialización bajo demanda [9] (Ver Listado 16.6 [10] del texto citado anteriormente).
// Corregir la inicialización diferida en la clase Java Foo { HelperHolder de clase estática privada { Helper final estático público helper = new Helper (); } public static Helper getHelper () { return HelperHolder . ayudante ; } }
Esto se basa en el hecho de que las clases anidadas no se cargan hasta que se hace referencia a ellas.
Semántica de El campo final en Java 5 se puede emplear para publicar de forma segura el objeto auxiliar sin usar volátil : [11]
public class FinalWrapper < T > { valor de T final público ; public FinalWrapper ( valor T ) { this . valor = valor ; } } public class Foo { private FinalWrapper < Helper > helperWrapper ; public Helper getHelper () { FinalWrapper < Helper > tempWrapper = helperWrapper ; if ( tempWrapper == null ) { sincronizado ( esto ) { if ( helperWrapper == null ) { helperWrapper = new FinalWrapper < Helper > ( new Helper ()); } tempWrapper = helperWrapper ; } } devuelve tempWrapper . valor ; } }
La variable local tempWrapper es necesario para la corrección: simplemente usando helperWrapper para las comprobaciones nulas y la declaración de devolución podría fallar debido a la reordenación de lectura permitida en el modelo de memoria de Java. [12] El rendimiento de esta implementación no es necesariamente mejor que el implementación volátil .
Uso en C #
El bloqueo con doble verificación se puede implementar de manera eficiente en .NET. Un patrón de uso común es agregar un bloqueo de doble verificación a las implementaciones Singleton:
clase pública MySingleton { objeto estático privado _myLock = nuevo objeto (); privado estático MySingleton _mySingleton = null ; privado MySingleton () { } public static MySingleton GetInstance () { if ( _mySingleton es nulo ) // La primera comprobación { lock ( _myLock ) { if ( _mySingleton es nulo ) // La segunda (doble) comprobación { _mySingleton = new MySingleton (); } } } return _mySingleton ; } }
En este ejemplo, la "sugerencia de bloqueo" es el objeto _mySingleton que ya no es nulo cuando está completamente construido y listo para usar.
En .NET Framework 4.0, Lazy
se introdujo la clase, que internamente usa el bloqueo doble verificado de forma predeterminada (modo ExecutionAndPublication) para almacenar la excepción que se lanzó durante la construcción o el resultado de la función que se pasó a Lazy
: [13]
public class MySingleton { privado estático de solo lectura Lazy < MySingleton > _mySingleton = new Lazy < MySingleton > (() => new MySingleton ()); privado MySingleton () { } instancia pública estática MySingleton => _mySingleton . Valor ; }
Ver también
- El lenguaje Test y Test-and-set para un mecanismo de bloqueo de bajo nivel.
- Lenguaje de titular de inicialización bajo demanda para un reemplazo seguro para subprocesos en Java.
Referencias
- ^ Schmidt, D y col. Arquitectura de software orientada a patrones Vol 2, 2000 pp353-363
- ^ a b David Bacon y col. La declaración "El bloqueo verificado dos veces está roto" .
- ^ "Soporte para características de C ++ 14-11-17 (C ++ moderno)" .
- ^ El bloqueo de doble verificación se corrige en C ++ 11
- ^ Boehm, Hans-J (junio de 2005). "Los subprocesos no se pueden implementar como una biblioteca" (PDF) . Avisos ACM SIGPLAN . 40 (6): 261–268. doi : 10.1145 / 1064978.1065042 .
- ^ Haggar, Peter (1 de mayo de 2002). "Bloqueo de doble verificación y el patrón Singleton" . IBM.
- ^ Joshua Bloch "Java efectivo, tercera edición", p. 372
- ^ "Capítulo 17. Hilos y bloqueos" . docs.oracle.com . Consultado el 28 de julio de 2018 .
- ^ Brian Goetz y col. Concurrencia de Java en la práctica, 2006 págs348
- ^ Goetz, Brian; et al. "Concurrencia de Java en la práctica - listados en el sitio web" . Consultado el 21 de octubre de 2014 .
- ^ [1] Lista de correo de discusión de Javamemorymodel
- ^ [2]Manson, Jeremy (14 de diciembre de 2008). "Date-Race-Ful Lazy Initialization for Performance - Java Concurrency (& c)" . Consultado el 3 de diciembre de 2016 .
- ^ Albahari, Joseph (2010). "Subprocesos en C #: Uso de subprocesos" . C # 4.0 en pocas palabras . O'Reilly Media. ISBN 978-0-596-80095-6.
Lazy
realmente implementa […] bloqueo doble verificado. El bloqueo con doble verificación realiza una lectura volátil adicional para evitar el costo de obtener un bloqueo si el objeto ya está inicializado.
enlaces externos
- Problemas con el mecanismo de bloqueo de doble verificación capturados en los blogs de Jeu George
- Descripción de "Bloqueo con doble verificación" del repositorio de patrones de Portland
- "El bloqueo con doble verificación está roto" Descripción del repositorio de patrones de Portland
- Documento " C ++ y los peligros del bloqueo con doble verificación " (475 KB) de Scott Meyers y Andrei Alexandrescu
- Artículo " Bloqueo revisado dos veces: inteligente, pero roto " por Brian Goetz
- Artículo " ¡Advertencia! Enhebrado en un mundo multiprocesador " por Allen Holub
- Bloqueo con doble verificación y patrón Singleton
- Seguridad de hilo y patrón singleton
- palabra clave volátil en VC ++ 2005
- Ejemplos de Java y sincronización de soluciones de bloqueo de doble verificación
- "Java más eficaz con Joshua Bloch de Google" .