En informática , un bloqueo o mutex (de mutual exclusion ) es una primitiva de sincronización que impide que varios subprocesos de ejecución modifiquen o accedan a un estado a la vez. Los bloqueos aplican 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.
En general, los bloqueos son bloqueos consultivos , en los que cada subproceso coopera adquiriendo el bloqueo antes de acceder a los datos correspondientes. Algunos sistemas también implementan bloqueos obligatorios , en los que intentar acceder sin autorización a un recurso bloqueado forzará una excepción en la entidad que intente 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 proporcionan 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 por 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 hilo 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 eficiente si los hilos están bloqueados por un corto tiempo, porque evita la sobrecarga de la reprogramación de procesos del sistema operativo. Es ineficiente si el bloqueo se mantiene durante mucho tiempo o si el progreso del hilo que mantiene el bloqueo depende de la preempción del hilo bloqueado.
Los bloqueos suelen requerir soporte de hardware para una implementación eficiente. Este soporte suele adoptar la forma de una o más instrucciones atómicas , como " test-and-set ", " fetch-and-add " o " compare-and-swap ". Estas instrucciones permiten que un único proceso pruebe si el bloqueo está libre y, si lo está, lo adquiera en una única operación atómica.
Las arquitecturas monoprocesador tienen la opción de utilizar secuencias ininterrumpibles de instrucciones (utilizando instrucciones especiales o prefijos de instrucciones para desactivar las interrupciones temporalmente), pero esta técnica no funciona en máquinas multiprocesador con memoria compartida. El soporte adecuado para bloqueos en un entorno multiprocesador puede requerir un 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 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. Como ambas tareas detectarán que el bloqueo está libre, ambas intentarán establecer el bloqueo, sin saber que la otra tarea también lo está estableciendo. El algoritmo de Dekker o el de Peterson son posibles sustitutos si no están disponibles las operaciones de bloqueo atómico.
El uso descuidado de los bloqueos puede dar lugar a un bloqueo mutuo o un bloqueo activo . Se pueden utilizar varias estrategias para evitar o recuperarse de los bloqueos mutuos o los 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 de modo que las combinaciones de bloqueos interdependientes siempre se adquieran en un orden de "cascada" específicamente definido).
Algunos lenguajes admiten bloqueos sintácticamente. A continuación, se muestra un ejemplo en C# :
clase pública Cuenta // Este es un monitor de una cuenta { decimal privado _balance = 0 ; objeto privado _balanceLock = nuevo objeto (); public void Deposit ( decimal amount ) { // Solo un hilo a la vez puede ejecutar esta declaración. lock ( _balanceLock ) { _balance += amount ; } } public void Withdraw ( decimal amount ) { // Solo un hilo a la vez puede ejecutar esta declaración. lock ( _balanceLock ) { _balance -= amount ; } } }
El código lock(this)
puede generar problemas si se puede acceder a la instancia de forma pública. [1]
De manera similar a Java , C# también puede sincronizar métodos completos, utilizando el atributo MethodImplOptions.Synchronized. [2] [3]
[MethodImpl(MethodImplOptions.Synchronized)] public void SomeMethod () { // hacer cosas }
Antes de familiarizarse con la granularidad de los bloqueos, es necesario comprender tres conceptos sobre ellos:
Existe un equilibrio entre reducir la sobrecarga de bloqueo y reducir la contención de bloqueo al elegir la cantidad de bloqueos en la sincronización.
Una propiedad importante de un bloqueo es su granularidad . La granularidad es una medida de la cantidad de datos que el bloqueo está protegiendo. En general, la elección de una granularidad gruesa (una pequeña cantidad de bloqueos, cada uno protegiendo 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 varios procesos se ejecutan simultáneamente. Esto se debe a una mayor contención de bloqueo . Cuanto más grueso sea el bloqueo, mayor será la probabilidad de que el bloqueo impida que un proceso no relacionado continúe. Por el contrario, el uso de una granularidad fina (una mayor cantidad de bloqueos, cada uno protegiendo una cantidad bastante pequeña de datos) aumenta la sobrecarga de los propios bloqueos, pero reduce la contención de bloqueo. El bloqueo granular, donde cada proceso debe mantener 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 introduzca sin saberlo un punto muerto . [ cita requerida ]
En un sistema de gestión de bases de datos , por ejemplo, un bloqueo podría proteger, en orden de granularidad decreciente, parte de un campo, un campo, un registro, una página de datos o una tabla entera. 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 múltiples usuarios.
Los bloqueos de bases de datos se pueden utilizar como un medio para garantizar la sincronicidad de las transacciones, es decir, cuando se realiza un procesamiento de transacciones concurrente (intercalado de transacciones), el uso de bloqueos de dos fases garantiza que la ejecución concurrente de la transacción resulte equivalente a algún orden serial de la transacción. Sin embargo, los bloqueos se convierten en un desafortunado efecto secundario del bloqueo en las bases de datos. Los bloqueos se previenen al predeterminar el orden de bloqueo entre transacciones o se detectan utilizando gráficos de espera . Una alternativa al bloqueo para la sincronización de la base de datos mientras se evitan los bloqueos implica el uso de marcas de tiempo globales totalmente ordenadas.
Existen mecanismos que se emplean para administrar las acciones de varios usuarios simultáneos en una base de datos; el objetivo es evitar la pérdida de actualizaciones y las lecturas sucias. Los dos tipos de bloqueo son el bloqueo pesimista y el bloqueo optimista :
Existen varias variaciones y refinamientos de estos tipos de bloqueo principales, con sus respectivas variaciones de comportamiento de bloqueo. Si un primer bloqueo bloquea otro bloqueo, los dos bloqueos se denominan incompatibles ; de lo contrario, los bloqueos son compatibles . A menudo, los tipos de bloqueo que bloquean las interacciones se presentan en la literatura técnica mediante una tabla de compatibilidad de bloqueos . El siguiente es un ejemplo con los tipos de bloqueo principales más comunes:
Tipo de bloqueo | bloqueo de lectura | bloqueo de escritura |
---|---|---|
bloqueo de lectura | ✔ | incógnita |
bloqueo de escritura | incógnita | incógnita |
Comentario: En algunas publicaciones, las entradas de la tabla están simplemente 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 tokens serializadores pueden evitar el mayor problema: los bloqueos. Las alternativas al bloqueo incluyen métodos de sincronización no bloqueantes , como técnicas de programación sin bloqueos 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 aplicación de los detalles de implementación de bloqueos, y los problemas enumerados anteriormente aún deben resolverse por 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 atómica del flujo de instrucciones (por ejemplo, la adición o eliminación de un elemento en una tubería requiere que todas las operaciones contemporáneas que necesiten agregar o eliminar otros elementos en la tubería se suspendan durante la manipulación del contenido de memoria necesario 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 sobre un sistema operativo y es capaz de reconocer con elegancia el informe de demandas imposibles. [ cita requerida ]
Uno de los mayores problemas de la programación basada en bloqueos es que "los bloqueos no se 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ña una clase Account que permite a varios clientes concurrentes depositar o retirar dinero a una cuenta, y proporciona un algoritmo para transferir dinero de una cuenta a otra.
La solución basada en bloqueo para la primera parte del problema es:
Clase Cuenta: miembro saldo: miembro entero mutex: bloqueo método deposito(n: entero) bloqueo mutex() equilibrio ← equilibrio + 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
función transferir(desde: Cuenta, a: Cuenta, importe: Entero) desde.retirar(cantidad) a.depositar(cantidad)
En un programa concurrente, este algoritmo es incorrecto porque cuando un hilo está a mitad de la transferencia , otro puede observar un estado en el que se ha retirado una cantidad 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 colocando bloqueos en ambas cuentas antes de cambiar cualquiera de ellas, pero luego los bloqueos se deben colocar de acuerdo con un orden arbitrario y global para evitar un bloqueo:
función transferir(desde: Cuenta, hasta: Cuenta, monto: Entero) si desde < hasta // orden arbitrario en los bloqueos desde.lock() para bloquear() demás para bloquear() desde.lock() desde.retirar(cantidad) a.depositar(cantidad) desde.unlock() 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 con estructuras de datos seguras para la concurrencia.@synchronized
[12] para colocar bloqueos en bloques de código y también proporciona las clases NSLock, [13] NSRecursiveLock, [14] y NSConditionLock [15] junto con el protocolo NSLocking [16] para el bloqueo 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 la MVar
, dejándola vacía, y lo vuelve a poner cuando termina. Intentar tomar un recurso de una , vacía, MVar
hace 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. Sin embargo, difieren en cómo se utilizan. Si bien un semáforo binario puede denominarse coloquialmente mutex, un mutex verdadero 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 abordar 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}}
: CS1 maint: nombres numéricos: lista de autores ( enlace ){{cite book}}
: CS1 maint: nombres numéricos: lista de autores ( enlace )