Seguridad del hilo - Thread safety

La seguridad de subprocesos es un concepto de programación informática aplicable al código de subprocesos múltiples . 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 interacción involuntaria. Existen 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 de subprocesos múltiples al restablecer algunas de las correspondencias entre el flujo de control real y el texto del programa, mediante sincronización .

Niveles de seguridad para subprocesos

Las bibliotecas de software pueden proporcionar ciertas garantías de seguridad para 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.

Los diferentes proveedores utilizan una terminología ligeramente diferente para la seguridad de los subprocesos:

  • Seguro para subprocesos : se garantiza que la implementación está libre de condiciones de carrera cuando se accede a varios subprocesos simultáneamente.
  • Condicionalmente seguro : diferentes subprocesos pueden acceder a diferentes objetos simultáneamente, y el acceso a los datos compartidos está protegido de las condiciones de carrera.
  • No es seguro para subprocesos : diferentes subprocesos no deben acceder simultáneamente a las estructuras de datos.

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 interbloqueo, ya que los interbloqueos pueden ser causados ​​por devoluciones de llamada y violación de capas arquitectónicas independientes de la propia biblioteca.

Enfoques de implementación

A continuación, analizamos dos clases de enfoques para evitar las condiciones de carrera para lograr la seguridad de los hilos.

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

Reentrada
Escribir código de tal manera que pueda ser parcialmente ejecutado por un subproceso, ejecutado por el mismo subproceso o simultáneamente ejecutado por otro subproceso 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 están localizadas para que cada hilo tenga su propia copia privada. Estas variables conservan sus valores a través de subrutinas y otros límites de código y son seguras para subprocesos, ya que son locales para cada subproceso, aunque el código que accede a ellas puede ser ejecutado simultáneamente por otro subproceso.
Objetos inmutables
El estado de un objeto no se puede cambiar después de la construcción. Esto implica tanto que solo se comparten datos de solo lectura como que se logra la seguridad de subprocesos inherente. Las operaciones mutables (no constantes) se pueden implementar de tal 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 estar bien pensada, ya que el uso inadecuado puede provocar efectos secundarios como puntos muertos , bloqueos en vivo y falta 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 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 de

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

class Counter {
    private int i = 0;

    public synchronized void inc() {
        i++;
    }
}

En el lenguaje de programación C , cada hilo tiene su propia pila. Sin embargo, una variable estática no se mantiene en la pila; todos los hilos 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 la 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 para evitar esto es utilizar otra variable compartida como un "bloqueo" o "mutex" (desde mut UAL ex conclusión).

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

# include <pthread.h>

int increment_counter ()
{
  static int counter = 0;
  static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

  // only allow one thread to increment at a time
  pthread_mutex_lock(&mutex);

  ++counter;

  // store value before any other threads increment it further
  int result = counter;

  pthread_mutex_unlock(&mutex);

  return result;
}

En lo anterior, increment_counter puede ser llamado por diferentes hilos sin ningún problema ya que se usa un mutex para sincronizar todos los accesos a la counter variable compartida . Pero si la función se usa en un manejador de interrupciones reentrantes 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 sufrir.

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

# include <atomic>

int increment_counter ()
{
  static std::atomic<int> counter(0);

  // increment is guaranteed to be done atomically
  int result = ++counter;

  return result;
}

Ver también

Referencias

enlaces externos