En informática , un bloqueo o mutex (de exclusión mutua ) es una primitiva de sincronización que evita que varios subprocesos de ejecución modifiquen el estado o accedan a él a la vez. Los bloqueos imponen políticas de control de concurrencia de exclusión mutua y, con una variedad de métodos posibles, existen múltiples implementaciones únicas para diferentes aplicaciones.
Generalmente, los bloqueos son bloqueos de asesoramiento , donde cada subproceso coopera adquiriendo el bloqueo antes de acceder a los datos correspondientes. Algunos sistemas también implementan bloqueos obligatorios , donde intentar acceder no autorizado a un recurso bloqueado forzará una excepción en la entidad que intenta realizar el acceso.
El tipo de bloqueo más simple es un semáforo binario . Proporciona acceso exclusivo a los datos bloqueados. Otros esquemas también brindan acceso compartido para leer datos. Otros modos de acceso ampliamente implementados son el exclusivo, el de intención de exclusión y el de intención de actualización.
Otra forma de clasificar los bloqueos es según lo que sucede cuando la estrategia de bloqueo impide el progreso de un hilo. La mayoría de los diseños de bloqueo bloquean la ejecución del subproceso que solicita el bloqueo hasta que se le permite acceder al recurso bloqueado. Con un spinlock , el hilo simplemente espera ("gira") hasta que el bloqueo esté disponible. Esto es eficaz si los subprocesos se bloquean durante un breve periodo de tiempo, porque evita la sobrecarga de la reprogramación de procesos del sistema operativo. Es ineficaz si el bloqueo se mantiene durante mucho tiempo o si el progreso del subproceso que mantiene el bloqueo depende de la preferencia del subproceso bloqueado.
Las cerraduras normalmente requieren soporte de hardware para una implementación eficiente. Este soporte suele tomar la forma de una o más instrucciones atómicas como " probar y configurar ", " buscar y agregar " o " comparar e intercambiar ". Estas instrucciones permiten que un solo proceso pruebe si el candado está libre y, si está libre, adquirir el candado en una sola operación atómica.
Las arquitecturas monoprocesador tienen la opción de utilizar secuencias ininterrumpibles de instrucciones (usando instrucciones especiales o prefijos de instrucciones para desactivar las interrupciones temporalmente), pero esta técnica no funciona para máquinas multiprocesador de memoria compartida. El soporte adecuado para bloqueos en un entorno multiprocesador puede requerir soporte de hardware o software bastante complejo, con importantes problemas de sincronización .
La razón por la que se requiere una operación atómica es por la concurrencia, donde más de una tarea ejecuta la misma lógica. Por ejemplo, considere el siguiente código C :
if ( lock == 0 ) { // bloqueo libre, configúrelo lock = myPID ; }
El ejemplo anterior no garantiza que la tarea tenga el bloqueo, ya que más de una tarea puede estar probando el bloqueo al mismo tiempo. Dado que ambas tareas detectarán que el bloqueo está libre, ambas tareas intentarán establecer el bloqueo, sin saber que la otra tarea también lo está configurando. Los algoritmos de Dekker o Peterson son posibles sustitutos si las operaciones de bloqueo atómico no están disponibles.
El uso descuidado de las cerraduras puede provocar un punto muerto o un bloqueo activo . Se pueden utilizar varias estrategias para evitar o recuperarse de interbloqueos o bloqueos activos, tanto en tiempo de diseño como en tiempo de ejecución . (La estrategia más común es estandarizar las secuencias de adquisición de bloqueos para que las combinaciones de bloqueos interdependientes siempre se adquieran en un orden de "cascada" específicamente definido).
Algunos idiomas admiten bloqueos sintácticamente. A continuación se muestra un ejemplo en C# :
Cuenta de clase pública // Este es un monitor de una cuenta { balance decimal privado = 0 ; objeto privado _balanceLock = nuevo objeto (); public void Deposit ( cantidad decimal ) { // Solo un hilo a la vez puede ejecutar esta declaración. bloquear ( _balanceLock ) { _saldo += monto ; } } public void Withdraw ( cantidad decimal ) { // Solo un hilo a la vez puede ejecutar esta declaración. bloquear ( _balanceLock ) { _saldo -= cantidad ; } } }
El código lock(this)
puede generar problemas si se puede acceder públicamente a la instancia. [1]
Al igual que Java , C# también puede sincronizar métodos completos mediante el uso del atributo MethodImplOptions.Synchronized. [2] [3]
[MethodImpl(MethodImplOptions.Synchronized)] public void SomeMethod () { // hacer cosas }
Antes de comenzar con la granularidad de los bloqueos, es necesario comprender tres conceptos sobre los bloqueos:
Existe una compensación entre disminuir la sobrecarga de bloqueo y disminuir la contención de bloqueo al elegir la cantidad de bloqueos en sincronización.
Una propiedad importante de una cerradura es su granularidad . La granularidad es una medida de la cantidad de datos que protege el bloqueo. En general, elegir una granularidad gruesa (una pequeña cantidad de bloqueos, cada uno de los cuales protege un gran segmento de datos) da como resultado una menor sobrecarga de bloqueo cuando un solo proceso accede a los datos protegidos, pero un peor rendimiento cuando se ejecutan varios procesos al mismo tiempo. Esto se debe a una mayor contención de bloqueos . Cuanto más burdo sea el bloqueo, mayor será la probabilidad de que detenga el desarrollo de un proceso no relacionado. Por el contrario, el uso de una granularidad fina (una mayor cantidad de bloqueos, cada uno de los cuales protege una cantidad bastante pequeña de datos) aumenta la sobrecarga de los propios bloqueos, pero reduce la contención de bloqueos. El bloqueo granular donde cada proceso debe contener múltiples bloqueos de un conjunto común de bloqueos puede crear dependencias de bloqueo sutiles. Esta sutileza puede aumentar la posibilidad de que un programador, sin saberlo, introduzca un punto muerto . [ cita necesaria ]
En un sistema de gestión de bases de datos , por ejemplo, un candado podría proteger, en orden de granularidad decreciente, parte de un campo, un campo, un registro, una página de datos o una tabla completa. La granularidad gruesa, como el uso de bloqueos de tabla, tiende a brindar el mejor rendimiento para un solo usuario, mientras que la granularidad fina, como los bloqueos de registros, tiende a brindar el mejor rendimiento para varios usuarios.
Los bloqueos de bases de datos se pueden utilizar como medio para garantizar la sincronicidad de las transacciones. es decir, cuando se hace que el procesamiento de transacciones sea concurrente (transacciones entrelazadas), el uso de bloqueos de 2 fases garantiza que la ejecución concurrente de la transacción resulte equivalente a algún orden en serie de la transacción. Sin embargo, los interbloqueos se convierten en un desafortunado efecto secundario del bloqueo de bases de datos. Los interbloqueos se evitan predeterminando el orden de bloqueo entre transacciones o se detectan mediante gráficos de espera . Una alternativa al bloqueo para la sincronización de la base de datos y al mismo tiempo evitar interbloqueos implica el uso de marcas de tiempo globales totalmente ordenadas.
Existen mecanismos empleados para gestionar las acciones de varios usuarios simultáneos en una base de datos; el propósito es evitar la pérdida de actualizaciones y lecturas sucias. Los dos tipos de bloqueo son el bloqueo pesimista y el bloqueo optimista :
Existen varias variaciones y refinamientos de estos tipos principales de cerraduras, con sus respectivas variaciones de comportamiento de bloqueo. Si un primer candado bloquea otro candado, los dos candados se denominan incompatibles ; De lo contrario, las cerraduras son compatibles . A menudo, los tipos de cerraduras que bloquean las interacciones se presentan en la literatura técnica mediante una tabla de compatibilidad de cerraduras . El siguiente es un ejemplo con los principales tipos de bloqueo comunes:
Comentario: En algunas publicaciones, las entradas de la tabla simplemente están marcadas como "compatibles" o "incompatibles", o respectivamente "sí" o "no". [5]
La protección de recursos basada en bloqueos y la sincronización de subprocesos/procesos tienen muchas desventajas:
Algunas estrategias de control de concurrencia evitan algunos o todos estos problemas. Por ejemplo, un embudo o serializar tokens puede evitar el mayor problema: los interbloqueos. Las alternativas al bloqueo incluyen métodos de sincronización sin bloqueo , como técnicas de programación sin bloqueo y memoria transaccional . Sin embargo, estos métodos alternativos a menudo requieren que los mecanismos de bloqueo reales se implementen en un nivel más fundamental del software operativo. Por lo tanto, es posible que solo liberen al nivel de la aplicación de los detalles de la implementación de bloqueos, y los problemas enumerados anteriormente aún deben tratarse debajo de la aplicación.
En la mayoría de los casos, el bloqueo adecuado depende de que la CPU proporcione un método de sincronización del flujo de instrucciones atómicas (por ejemplo, la adición o eliminación de un elemento en una canalización requiere que todas las operaciones contemporáneas que necesiten agregar o eliminar otros elementos en la canalización se suspendan durante la manipulación del contenido de la memoria necesaria para agregar o eliminar el elemento específico). Por lo tanto, una aplicación a menudo puede ser más robusta cuando reconoce las cargas que impone a un sistema operativo y es capaz de reconocer amablemente el informe de demandas imposibles. [ cita necesaria ]
Uno de los mayores problemas de la programación basada en bloqueos es que "los bloqueos no componen ": es difícil combinar módulos pequeños y correctos basados en bloqueos en programas más grandes igualmente correctos sin modificar los módulos o al menos conocer sus componentes internos. Simon Peyton Jones (un defensor de la memoria transaccional de software ) da el siguiente ejemplo de una aplicación bancaria: [6] diseñar una clase Cuenta que permita a múltiples clientes simultáneos depositar o retirar dinero a una cuenta; y dar un algoritmo para transferir dinero de una cuenta a otra. La solución basada en bloqueos para la primera parte del problema es:
cuenta de clase : saldo de miembro : mutex de miembro entero : bloqueo método de depósito (n: entero) mutex.bloqueo() saldo ← saldo + n mutex.desbloquear() método retirar (n: entero) depósito(-n)
La segunda parte del problema es mucho más complicada. Una rutina de transferencia que sea correcta para programas secuenciales sería
transferencia de función (de: Cuenta, a: Cuenta, monto: Entero) de.retirar(monto) a.depositar(monto)
En un programa concurrente, este algoritmo es incorrecto porque cuando un hilo está a la mitad de la transferencia , otro puede observar un estado en el que se ha retirado un monto de la primera cuenta, pero aún no se ha depositado en la otra cuenta: el dinero ha desaparecido del sistema. Este problema solo se puede solucionar por completo tomando bloqueos en ambas cuentas antes de cambiar cualquiera de las dos cuentas, pero luego los bloqueos deben tomarse de acuerdo con algún orden global arbitrario para evitar un punto muerto:
transferencia de función (de: Cuenta, a: Cuenta, monto: Entero) si de < a // ordenamiento arbitrario en las cerraduras desde.lock() para bloquear() demás para bloquear() desde.lock() de.retirar(monto) a.depositar(monto) desde.desbloquear() para desbloquear()
Esta solución se vuelve más complicada cuando hay más bloqueos involucrados y la función de transferencia necesita conocer todos los bloqueos, por lo que no se pueden ocultar .
Los lenguajes de programación varían en su soporte para la sincronización:
synchronize
lock
palabra clave en un hilo para garantizar su acceso exclusivo a un recurso.SyncLock
palabra clave como la palabra clave de C# lock
.synchronized
para bloquear bloques de código, métodos u objetos [11] y bibliotecas que presentan estructuras de datos seguras para la concurrencia.@synchronized
[12] para bloquear bloques de código y también proporciona las clases NSLock, [13] NSRecursiveLock, [14] y NSConditionLock [15] junto con el protocolo NSLocking [16] para bloquear también.Mutex
clase en la pthreads
extensión. [18]Lock
clase del threading
módulo. [19]lock_type
tipo derivado en el módulo intrínseco iso_fortran_env
y las declaraciones lock
/ unlock
desde Fortran 2008 . [20]Mutex<T>
[22] . [23]LOCK
prefijo en ciertas operaciones para garantizar su atomicidad.MVar
, que puede estar vacía o contener un valor, normalmente una referencia a un recurso. Un hilo que quiere utilizar el recurso 'toma' el valor de MVar
, lo deja vacío y lo devuelve cuando termina. Intentar tomar un recurso de un lugar vacío MVar
da como resultado que el hilo se bloquee hasta que el recurso esté disponible. [24] Como alternativa al bloqueo, también existe una implementación de memoria transaccional de software . [25]Un mutex es un mecanismo de bloqueo que a veces utiliza la misma implementación básica que el semáforo binario. Las diferencias entre ellos están en cómo se utilizan. Si bien un semáforo binario puede denominarse coloquialmente mutex, un verdadero mutex tiene un caso de uso y una definición más específicos, ya que se supone que solo la tarea que bloqueó el mutex debe desbloquearlo. Esta restricción tiene como objetivo manejar algunos problemas potenciales del uso de semáforos:
Un objeto protegido proporciona acceso coordinado a datos compartidos, a través de llamadas a sus operaciones protegidas visibles, que pueden ser subprogramas protegidos o entradas protegidas.
{{cite book}}
: Mantenimiento CS1: nombres numéricos: lista de autores ( enlace ){{cite book}}
: Mantenimiento CS1: nombres numéricos: lista de autores ( enlace )