stringtranslate.com

Bloqueo (informática)

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.

Tipos

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 }   

Granularidad

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.

Bloqueos de bases de datos

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 (transacciones intercaladas), 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 :

Dónde utilizar el bloqueo pesimista: se utiliza principalmente en entornos donde la contención de datos (el grado de solicitudes de los usuarios al sistema de base de datos en un momento dado) es alta; donde el costo de proteger los datos mediante bloqueos es menor que el costo de revertir las transacciones, si ocurren conflictos de concurrencia. La concurrencia pesimista se implementa mejor cuando los tiempos de bloqueo serán cortos, como en el procesamiento programático de registros. La concurrencia pesimista requiere una conexión persistente a la base de datos y no es una opción escalable cuando los usuarios interactúan con los datos, porque los registros pueden estar bloqueados durante períodos de tiempo relativamente largos. No es adecuado para su uso en el desarrollo de aplicaciones web.
Dónde utilizar el bloqueo optimista: esto es apropiado en entornos donde hay poca contención de datos o donde se requiere acceso de solo lectura a los datos. La concurrencia optimista se utiliza ampliamente en .NET para abordar las necesidades de aplicaciones móviles y desconectadas, [4] donde el bloqueo de filas de datos durante períodos prolongados de tiempo sería inviable. Además, mantener los bloqueos de registros requiere una conexión persistente al servidor de base de datos, lo que no es posible en aplicaciones desconectadas.

Tabla de compatibilidad de cerraduras

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:

Comentario: En algunas publicaciones, las entradas de la tabla están simplemente marcadas como "compatibles" o "incompatibles", o respectivamente "sí" o "no". [5]

Desventajas

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 sin bloqueo , 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 ]

Falta de componibilidad

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ñe una clase Cuenta que permita a varios clientes concurrentes depositar o retirar dinero a una cuenta, y proporcione 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 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 .

Soporte de idiomas

Los lenguajes de programación varían en su soporte para la sincronización:

Mutexes vs. semáforos

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:

  1. Inversión de prioridad : si el mutex sabe quién lo bloqueó y se supone que debe desbloquearlo, es posible promover la prioridad de esa tarea siempre que una tarea de mayor prioridad comience a esperar en el mutex.
  2. Terminación prematura de tareas: los mutex también pueden brindar seguridad de eliminación, donde la tarea que contiene el mutex no puede eliminarse accidentalmente. [ cita requerida ]
  3. Bloqueo de terminación: si una tarea que mantiene un mutex finaliza por cualquier motivo, el sistema operativo puede liberar el mutex y las tareas de espera de señal de esta condición.
  4. Bloqueo de recursión: a una tarea se le permite bloquear un mutex reentrante varias veces mientras lo desbloquea una cantidad igual de veces.
  5. Liberación accidental: se genera un error al liberar el mutex si la tarea que lo libera no es su propietario.

Véase también

Referencias

  1. ^ "Declaración de bloqueo (Referencia de C#)".
  2. ^ "ThreadPoolPriority y MethodImplAttribute". MSDN. pág. ?? . Consultado el 22 de noviembre de 2011 .
  3. ^ "C# desde la perspectiva de un desarrollador de Java". Archivado desde el original el 2 de enero de 2013. Consultado el 22 de noviembre de 2011 .
  4. ^ "Diseño de componentes de niveles de datos y paso de datos a través de niveles". Microsoft . Agosto de 2002. Archivado desde el original el 8 de mayo de 2008 . Consultado el 30 de mayo de 2008 .
  5. ^ "Protocolo de control de concurrencia basado en bloqueos en DBMS". GeeksforGeeks . 2018-03-07 . Consultado el 2023-12-28 .
  6. ^ Peyton Jones, Simon (2007). "Beautiful concurrency" (PDF) . En Wilson, Greg; Oram, Andy (eds.). Beautiful Code: Leading Programmers Explain How They Think (Código hermoso: los principales programadores explican cómo piensan) . O'Reilly.
  7. ^ ISO/IEC 8652:2007. "Unidades protegidas y objetos protegidos". Manual de referencia de Ada 2005. Consultado el 27 de febrero de 2010. 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 )
  8. ^ ISO/IEC 8652:2007. "Ejemplo de asignación de tareas y sincronización". Manual de referencia de Ada 2005. Consultado el 27 de febrero de 2010 .{{cite book}}: CS1 maint: nombres numéricos: lista de autores ( enlace )
  9. ^ Marshall, Dave (marzo de 1999). "Mutual Exclusion Locks" (Bloqueos de exclusión mutua) . Consultado el 30 de mayo de 2008 .
  10. ^ "Sincronizar". msdn.microsoft.com . Consultado el 30 de mayo de 2008 .
  11. ^ "Sincronización". Sun Microsystems . Consultado el 30 de mayo de 2008 .
  12. ^ "Referencia de subprocesamiento de Apple". Apple, Inc. Consultado el 17 de octubre de 2009 .
  13. ^ "Referencia de NSLock". Apple, Inc. Consultado el 17 de octubre de 2009 .
  14. ^ "Referencia de NSRecursiveLock". Apple, Inc. Consultado el 17 de octubre de 2009 .
  15. ^ "Referencia de NSConditionLock". Apple, Inc. Consultado el 17 de octubre de 2009 .
  16. ^ "Referencia del protocolo NSLocking". Apple, Inc. Consultado el 17 de octubre de 2009 .
  17. ^ "rebaño".
  18. ^ "La clase Mutex". Archivado desde el original el 4 de julio de 2017. Consultado el 29 de diciembre de 2016 .
  19. ^ Lundh, Fredrik (julio de 2007). "Mecanismos de sincronización de subprocesos en Python". Archivado desde el original el 1 de noviembre de 2020. Consultado el 30 de mayo de 2008 .
  20. ^ John Reid (2010). "Coarrays en el próximo estándar Fortran" (PDF) . Consultado el 17 de febrero de 2020 .
  21. ^ "Programación en Ruby: subprocesos y procesos". 2001. Consultado el 30 de mayo de 2008 .
  22. ^ "std::sync::Mutex - Rust". doc.rust-lang.org . Consultado el 3 de noviembre de 2020 .
  23. ^ "Concurrencia de estado compartido: el lenguaje de programación Rust". doc.rust-lang.org . Consultado el 3 de noviembre de 2020 .
  24. ^ Marlow, Simon (agosto de 2013). "Concurrencia básica: subprocesos y MVars". Programación paralela y concurrente en Haskell. O'Reilly Media . ISBN 9781449335946.
  25. ^ Marlow, Simon (agosto de 2013). "Memoria transaccional de software". Programación paralela y concurrente en Haskell. O'Reilly Media . ISBN 9781449335946.
  26. ^ "paquete de sincronización - sincronización - pkg.go.dev". pkg.go.dev . Consultado el 23 de noviembre de 2021 .

Enlaces externos