stringtranslate.com

Seguridad de los hilos

En la programación informática multiproceso , una función es segura para subprocesos cuando puede ser invocada o accedida simultáneamente por múltiples subprocesos sin causar un comportamiento inesperado, condiciones de carrera o corrupción de datos. [1] [2] Al igual que en el contexto multiproceso donde un programa ejecuta varios subprocesos simultáneamente en un espacio de direcciones compartido y cada uno de esos subprocesos tiene acceso a la memoria de todos los demás subprocesos , las funciones seguras para subprocesos deben garantizar que todos esos subprocesos se comporten correctamente y cumplan con sus especificaciones de diseño sin interacciones no deseadas. [3]

Existen varias estrategias para crear estructuras de datos seguras para subprocesos. [3]

Niveles de seguridad de subprocesos

Diferentes proveedores utilizan una terminología ligeramente diferente para la seguridad de subprocesos, [4] pero la terminología de seguridad de subprocesos más comúnmente utilizada es: [2]

Las garantías de seguridad de subprocesos también suelen incluir pasos de diseño para prevenir o limitar el riesgo de diferentes formas de bloqueos , así como optimizaciones para maximizar el rendimiento concurrente. Sin embargo, no siempre se pueden dar garantías de que no haya bloqueos, ya que estos pueden ser causados ​​por devoluciones de llamadas y violaciones de capas arquitectónicas independientes de la propia biblioteca.

Las bibliotecas de software pueden proporcionar ciertas garantías de seguridad para subprocesos. [5] Por ejemplo, se puede garantizar que las lecturas simultáneas sean seguras para subprocesos, pero las escrituras simultáneas podrían no serlo. El hecho de que un programa que utilice una biblioteca de este tipo sea seguro para subprocesos depende de si utiliza la biblioteca de una manera coherente con esas garantías.

Enfoques de implementación

A continuación, analizamos dos clases de enfoques para evitar condiciones de carrera y lograr la seguridad del subproceso.

La primera clase de enfoques se centra en evitar el estado compartido e incluye:

Reingreso [6]
Escribir código de tal manera que pueda ser ejecutado parcialmente por un hilo, ejecutado por el mismo hilo o ejecutado simultáneamente por otro hilo y aún así completar correctamente la ejecución original. Esto requiere guardar la información de estado en variables locales para cada ejecución, generalmente en una pila, en lugar de en variables estáticas o globales u otro estado no local. Se debe acceder a todos los estados no locales a través de operaciones atómicas y las estructuras de datos también deben ser reentrantes.
Almacenamiento local de subprocesos
Las variables se localizan de modo que cada subproceso tenga su propia copia privada. Estas variables conservan sus valores en los límites de subrutinas y otros códigos y son seguras para subprocesos, ya que son locales para cada subproceso, aunque el código que accede a ellas pueda ser ejecutado simultáneamente por otro subproceso.
Objetos inmutables
El estado de un objeto no se puede cambiar después de su construcción. Esto implica que solo se comparten datos de solo lectura y que se logra la seguridad inherente de los subprocesos. Las operaciones mutables (no constantes) se pueden implementar de manera que creen nuevos objetos en lugar de modificar los existentes. Este enfoque es característico de la programación funcional y también lo utilizan las implementaciones de cadenas en Java, C# y Python. (Consulte Objeto inmutable ).


La segunda clase de enfoques están relacionados con la sincronización y se utilizan en situaciones en las que no se puede evitar el estado compartido:

Exclusión mutua
El acceso a los datos compartidos se serializa mediante mecanismos que garantizan que solo un hilo lea o escriba en los datos compartidos en cualquier momento. La incorporación de la exclusión mutua debe considerarse cuidadosamente, ya que un uso inadecuado puede generar efectos secundarios como bloqueos , bloqueos activos y escasez de recursos .
Operaciones atómicas
Se accede a los datos compartidos mediante operaciones atómicas que no pueden ser interrumpidas por otros subprocesos. Esto generalmente requiere el uso de instrucciones especiales en lenguaje de máquina , que pueden estar disponibles en una biblioteca de tiempo de ejecución . Dado que las operaciones son atómicas, los datos compartidos siempre se mantienen en un estado válido, sin importar cómo accedan a ellos otros subprocesos. Las operaciones atómicas forman la base de muchos mecanismos de bloqueo de subprocesos y se utilizan para implementar primitivas de exclusión mutua.

Ejemplos

En el siguiente fragmento de código Java , la palabra clave Java sincronizada hace que el método sea seguro para subprocesos:

clase  Contador { int privado i = 0 ;       public sincronizado void inc () { i ++ ; } }      

En el lenguaje de programación C , cada subproceso tiene su propia pila. Sin embargo, una variable estática no se mantiene en la pila; todos los subprocesos comparten acceso simultáneo a ella. Si varios subprocesos se superponen mientras ejecutan la misma función, es posible que un subproceso cambie una variable estática mientras otro está a mitad de su verificación. Este error lógico difícil de diagnosticar , que puede compilarse y ejecutarse correctamente la mayor parte del tiempo, se denomina condición de carrera . Una forma común de evitar esto es usar otra variable compartida como un " bloqueo" o "mutex" (de exclusión mutua ).

En el siguiente fragmento de código C, la función es segura para subprocesos, pero no reentrante:

# incluir <pthread.h>  int increment_counter () { static int contador = 0 ; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;             // permitir que solo un hilo incremente a la vez pthread_mutex_lock ( & mutex );  ++ contador ; // almacena el valor antes de que cualquier otro hilo lo incremente aún más int result = counter ;     pthread_mutex_unlock ( & mutex ); devolver resultado ; } 

En el ejemplo anterior, increment_counterse puede llamar desde distintos subprocesos sin ningún problema, ya que se utiliza un mutex para sincronizar todos los accesos a la countervariable compartida. Pero si la función se utiliza en un controlador de interrupción reentrante y surge una segunda interrupción mientras el mutex está bloqueado, la segunda rutina se bloqueará para siempre. Como el servicio de interrupciones puede desactivar otras interrupciones, todo el sistema podría verse afectado.

La misma función se puede implementar para que sea segura para subprocesos y reentrante utilizando los átomos sin bloqueo en C++11 :

# incluir <atomic>  int increment_counter () { static std :: atómico < int > contador ( 0 );      // se garantiza que el incremento se realizará de forma atómica int resultado = ++ contador ;     devolver resultado ; } 

Véase también

Referencias

  1. ^ Kerrisk, Michael (2010). La interfaz de programación de Linux . No Starch Press . p. 699, "Capítulo 31: SUBPROCESOS: SEGURIDAD DE SUBPROCESOS Y ALMACENAMIENTO POR SUBPROCESOS"{{cite book}}: CS1 maint: postscript (link)
  2. ^ de Oracle (1 de noviembre de 2010). "Oracle: seguridad de subprocesos". Docs.oracle.com . Consultado el 16 de octubre de 2013. "Un procedimiento es seguro para subprocesos cuando es lógicamente correcto cuando se ejecuta simultáneamente por varios subprocesos"; "3 niveles de seguridad para subprocesos"{{cite web}}: CS1 maint: postscript (link)
  3. ^ de Oracle (noviembre de 2020). "Guía de programación multiproceso: Capítulo 7 Interfaces seguras e inseguras". Docs Oracle . Consultado el 30 de abril de 2024 ; "Seguridad de subprocesos"{{cite web}}: CS1 maint: postscript (link)
  4. ^ "Clasificaciones de seguridad de subprocesos de API". IBM. 2023-04-11 . Consultado el 2023-10-09 .
  5. ^ "Niveles de seguridad de MT para bibliotecas". Docs Oracle . Consultado el 17 de mayo de 2024 .
  6. ^ "Reentrada y seguridad de subprocesos | Qt 5.6". Proyecto Qt . Consultado el 20 de abril de 2016 .

Enlaces externos