Seguridad del hilo
Seguridad de subprocesos es un concepto de programación informática aplicable al código multiproceso. El código seguro para subprocesos solo manipula las estructuras de datos compartidas de una manera que garantiza que todos los subprocesos se comporten correctamente y cumplan con sus especificaciones de diseño sin interacciones no deseadas. Hay varias estrategias para crear estructuras de datos seguras para subprocesos.
Un programa puede ejecutar código en varios subprocesos simultáneamente en un espacio de direcciones compartido donde cada uno de esos subprocesos tiene acceso a prácticamente toda la memoria de todos los demás subprocesos. La seguridad de subprocesos es una propiedad que permite que el código se ejecute en entornos multiproceso al restablecer algunas de las correspondencias entre el flujo de control real y el texto del programa, mediante la sincronización.
Niveles de seguridad de subprocesos
Las bibliotecas de software pueden proporcionar ciertas garantías de seguridad de subprocesos. Por ejemplo, se puede garantizar que las lecturas simultáneas sean seguras para subprocesos, pero es posible que las escrituras simultáneas no lo sean. Si un programa que usa una biblioteca de este tipo es seguro para subprocesos depende de si usa la biblioteca de una manera consistente con esas garantías.
Diferentes proveedores usan una terminología ligeramente diferente para la seguridad de subprocesos:
- Thread safe: La implementación está garantizada a estar libre de condiciones de raza cuando se accede por múltiples hilos simultáneamente.
- A salvo condicional: Diferentes hilos pueden acceder a diferentes objetos simultáneamente, y el acceso a datos compartidos está protegido de las condiciones de raza.
- No hay hilo seguro: Las estructuras de datos no deben ser accedidas simultáneamente por diferentes hilos.
Las garantías de seguridad de subprocesos generalmente también incluyen pasos de diseño para prevenir o limitar el riesgo de diferentes formas de interbloqueos, así como optimizaciones para maximizar el rendimiento simultáneo. Sin embargo, no siempre se pueden dar garantías sin interbloqueos, ya que los interbloqueos pueden ser causados por devoluciones de llamadas y violaciones de capas arquitectónicas independientes de la propia biblioteca.
Enfoques de implementación
A continuación, analizamos dos clases de enfoques para evitar condiciones de carrera para lograr la seguridad de subprocesos.
La primera clase de enfoques se centra en evitar el estado compartido e incluye:
- Re-entrancy
- Escribir código de tal manera que pueda ser ejecutado parcialmente por un hilo, ejecutado por el mismo hilo, o simultáneamente ejecutado por otro hilo y todavía completar correctamente la ejecución original. Esto requiere el ahorro de información estatal 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. Todos los estados no locales deben ser accedidos a través de operaciones atómicas y las estructuras de datos también deben ser reentradas.
- Almacenamiento local
- Las variables se localizan para que cada hilo tenga su propia copia privada. Estas variables conservan sus valores a través de la subrutina y otros límites de código y son seguros de rosca ya que son locales a cada hilo, aunque el código que los accede puede ser ejecutado simultáneamente por otro hilo.
- Immutable objects
- El estado de un objeto no puede ser cambiado después de la construcción. Esto implica que sólo se comparten datos solo leídos y que se alcanza la seguridad inherente de los hilos. Las operaciones mutables (no contables) pueden ser implementadas de tal manera que crean nuevos objetos en lugar de modificar los existentes. Este enfoque es característico de la programación funcional y también es utilizado por el cuerda implementaciones en Java, C# y Python. (Ver objeto inmutable.)
La segunda clase de enfoques está relacionada con la sincronización y se utiliza en situaciones en las que no se puede evitar el estado compartido:
- Exclusión mutua
- Acceso a datos compartidos serializada usando mecanismos que aseguran que sólo un hilo lea o escribe a los datos compartidos en cualquier momento. La incorporación de la exclusión mutua debe ser bien pensada, ya que el uso indebido puede llevar a efectos secundarios como bloqueos, candados y hambre de recursos.
- Operaciones aéreas
- Los datos compartidos se acceden mediante operaciones atómicas que no pueden ser interrumpidas por otros hilos. Esto generalmente requiere el uso de instrucciones especiales de 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 guardan en un estado válido, no importa cómo otros hilos lo accedan. Las operaciones atómicas forman la base de muchos mecanismos de bloqueo de hilos, y se utilizan para implementar primitivos de exclusión mutua.
Ejemplos
En el siguiente fragmento de código Java, la palabra clave de Java sincronizado hace que el método sea seguro para subprocesos:
clase Contrato {} privado int i = 0; público sincronizado vacío 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 él. Si varios subprocesos se superponen mientras se ejecuta la misma función, es posible que un subproceso cambie una variable estática mientras que otro está a mitad de camino para verificarla. 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 "bloqueo" o "mutex" (de exclusiónmutuaual).
En el siguiente fragmento de código C, la función es segura para subprocesos, pero no reentrante:
# incluir - No.int increase_counter (){} estática int contra = 0; estática pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // sólo permite un hilo aumentar a la vez pthread_mutex_lock()"mutex); ++contra; // valor de la tienda antes de cualquier otro hilo aumentar más int resultado = contra; pthread_mutex_unlock()"mutex); retorno resultado;}
En lo anterior, diferentes subprocesos pueden llamar a increment_counter
sin ningún problema, ya que se usa un mutex para sincronizar todos los accesos a la variable counter
compartida. Pero si la función se usa 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 deshabilitar 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 atómicos sin bloqueo en C++ 11:
# incluir Identificadoint increase_counter (){} estática std::atómico.int■ contra()0); // El aumento está garantizado para ser hecho atómico int resultado = ++contra; retorno resultado;}
Contenido relacionado
Fuente de poder ininterrumpible
JUnit
Doctora V64