Corrupción de memoria

En esta lección os enseño un ejemplo claro de corrupción de memoria, cuando varios hilos tratan de utilizar un recurso compartido como puede ser una variable a incrementar, sin prestar atención al uso que otros hilos hacen de la misma.

La corrupción de memoria es un problema real en concurrencia que puede afectar a la forma en la que nuestro programa se ejecuta. Por ello, se requiere de cuidado a la hora de diseñar la forma en la que los hilos de un programa acceden a recursos compartidos tales como áreas de memoria visibles desde dos o más hilos.

Si los hilos sólo acceden para leer una zona de memoria que no cambia, por lo general la operación debería ser segura; pero si los hilos van a modificar los valores de memoria, o si puede que el valor que los hilos están leyendo cambie, habrá que tener cuidado para asegurarse de que los hilos no leen o escriben información desactualizada.

Un ejemplo de cómo puede ocurrir corrupción de memoria

Uno de los ejemplos más claros de fallos de acceso a memoria en un modelo concurrente es tratar de modificar una misma variable desde varios hilos de una manera no atómica.

Atómico querrá decir, como estudiaremos en el futuro en esta lista, que la operación de leer y actualizar el valor de una variable se hace en un único paso, sin posibilidad de que, por ejemplo, otro hilo empiece a acceder a una zona de memoria mientras otro hilo todavía está actualizando el valor de la variable.

Pensemos por ejemplo en incrementar una variable. El siguiente pseudocódigo toma el valor de un contador almacenado en un recurso compartido y lo incrementa en uno:

int valor = recurso.getTotal();
recurso.setTotal(valor + 1);

En este caso, el incremento está ocurriendo de manera no atómica. Primero obtenemos el valor, y luego lo volvemos a colocar en su sitio una vez actualizado.

Si existe la mínima posibilidad de que varios hilos puedan ejecutar esta zona de código a la vez, entonces hay que prepararse para la situación en la que dos hilos puedan ejecutar exactamente a la vez estas instrucciones, o bien para la situación en la que el scheduler de nuestra CPU decida dejar de ejecutar un hilo justo cuando ese hilo está entre ambas instrucciones.

En ambas situaciones, lo que puede ocurrir es lo siguiente: vamos a decir que inicialmente el total es 0 y que pretendemos que cada hilo use ese código para incrementar en 1 el valor de la variable. En total, el valor final del contador será 2 porque tenemos dos hilos. Si tuviésemos 10 hilos, el valor final debería ser 10, en tanto que lo que pretendemos es que cada hilo incremente una vez el valor de la variable total.

Esto es algo que podría ocurrir bien cuando el hilo A consigue ejecutar las dos instrucciones de forma simultánea antes de que el hilo B haga lo mismo. El orden de las instrucciones sería el siguiente:

  1. Hilo A ejecuta getTotal(), por lo que hilo A ahora tiene una variable local que vale 0.
  2. Hilo A ejecuta setTotal(), por lo que hilo A ahora pone el total a 0 + 1 = 1.
  3. Hilo B ejecuta getTotal(), por lo que hilo B ahora tiene una variable local que vale 1.
  4. Hilo A ejecuta setTotal(), por lo que hilo B ahora pone el total a 1 + 1 = 2.

El problema es que no hay garantías de que este sea el orden en el que se ejecuten las instrucciones. Por lo tanto, en alguna ocasión puede ocurrir lo siguiente en su lugar:

  1. Hilo A ejecuta getTotal(), por lo que hilo A ahora tiene una variable local que vale 0.
  2. Hilo B ejecuta getTotal(), por lo que hilo B ahora tiene una variable local que también vale 0.
  3. Hilo A ejecuta setTotal(), por lo que hilo A ahora pone el total a 0 + 1 = 1.
  4. Hilo B ejecuta setTotal(), pero como la variable local que había sacado B también vale 0, vuelve a poner el total como 0 + 1 = 1.

En este caso, debido a que uno de los hilos ha trabajado con una variable local desactualizada, lo que va a ocurrir es que se va a corromper la memoria, porque se va a hacer algo importante a partir de datos desactualizados.

¿Cómo impedir que pase esto?

Existen medios para impedir que varios hilos corrompan memoria o un recurso compartido. La concurrencia no solamente requiere estudiar las estructuras que nos permiten organizar el código de un programa (hilo, tarea, futuro...), sino que también exige estudiar las estructuras que nos permiten asegurar que esas otras estructuras acceden de forma segura a recursos compartidos.

Aquí englobamos cosas como los locks, los semáforos, las memory barriers. Estas cosas son muy relevantes a la hora de hacer programas concurrentes, y en este curso también tendremos que estudiar en qué consisten estas estructuras para que cuando debamos decidir cómo organizar el código de un programa concurrente podamos tomar en consideración todo lo que sabemos de concurrencia para decidir qué es lo mejor que podemos hacer para garantizar un acceso ordenado a un recurso.

En la siguiente lección, por ejemplo, empezaremos usando monitores para tratar de arreglar este ejemplo.

Lista de reproducción
  1. 1
    ¿Qué es la concurrencia?
    8 minutos
  2. 2
    ¿Qué es un hilo?
    5 minutos
  3. 3
    Crear un hilo en Java
    10 minutos
  4. 4
    Interrupción de hilos
    10 minutos
  5. 5
    Cómo usar Thread.join
    14 minutos
  6. 6
    Corrupción de memoria
    8 minutos
  7. 7
    Monitores y synchronized
    10 minutos
  8. 8
    Bloque synchronized
    12 minutos
  9. 9
    Preguntas típicas sobre synchronized en Java
    8 minutos
  10. 10
    Interbloqueos, synchronized y el problema de la cena de los filósofos
    13 minutos
  11. 11
    Introducción a Executor en Java
    11 minutos
  12. 12
    Introducción al uso de ExecutorService en Java
    14 minutos
  13. 13
    Introducción a Thread Pools en Java
    10 minutos
  14. 14
    Cómo crear Thread Pools en Java
    11 minutos
  15. 15
    ¿Cómo funcionan wait() y notify()?
    10 minutos
  16. 16
    Ejemplo de wait() y notify() en Java
    9 minutos
  17. 17
    La palabra clave volatile
    8 minutos
  18. 18
    Cómo usar Future
    8 minutos
  19. 19
    Variables atómicas
    7 minutos