Práctica 01 - ProgProcesosYServicios/Practicas-2 GitHub Wiki

Práctica 2.1: Condiciones de carrera

Como se ha visto, el peligro más importante en la programación concurrente es la compartición de recursos; si no se hace con cuidado pueden aparecer condiciones de carrera.

En esta práctica vamos a intentar poner de maniñesto este problema tratando de conseguir que nuestro programa sufra una condición de carrera (y podamos detectarlo). El ejemplo descrito antes, de dos hebras incrementan- do una variable compartida, puede ocasionar condiciones de carrera, pero es muy improbable (en monoprocesador), y no podemos forzarlo. De hecho, ese es el peor tipo de condiciones de carrera, las improbables, pues ocurren muy pocas veces, y cuando lo hacen es virtualmente imposible averiguar que ha ocurrido para que el programa haya dejado de funcionar.

Para conseguir una condición de carrera de este tipo (de actualización perdida) lo que vamos a hacer es intentar distanciar en el tiempo el momento en el que se lee el valor de una variable y el momento en el que se modifica con el nuevo valor tras realizar un cálculo:

package p01;

public class CondicionDeCarrera implements Runnable {

	/**
	 * Número que vamos a sumar al atributo _suma en el método
	 * run(). Pero lo haremos de uno en uno.
	 */
	public static final int NUMERO_SUMADO = 10000;

	/**
	 * Número de veces que vamos a sumar NUMERO_SUMADO al atributo
	 * _suma en el método run().
	 */
	public static final long NUM_VECES = 10000;

	/**
	 * Método estático que devuelve acumulador + n. Hace la
	 * suma de uno en uno con un for.
	 * 
	 * @param acumulador Valor inicial.
	 * @param n Valor a sumar
	 * @return acumulador + n
	 */
	private static long sumaN(long acumulador, int n) {

		long total = acumulador;
		for (int i = 0; i < n; ++i)
			total += 1;
		//Thread.yield();
		return total;
		
	} // sumaN
	
	//-----------------------------------------------------

	/**
	 * Método todo a ser ejecutado a través de una hebra. Llama
	 * NUM_VECES a sumaN para sumar NUMERO_SUMADO al atributo _suma.
	 */
	public void run() {
		
		for (int i = 1; i <= NUM_VECES; ++i)
			_suma = sumaN(_suma, NUMERO_SUMADO);

	} // run
	
	//-----------------------------------------------------

	/**
	 * Devuelve el valor del atributo _suma.
	 * 
	 * @return Valor del atributo _suma.
	 */
	public long getSuma() {
		
		return _suma;

	} // getSuma

	//-----------------------------------------------------
	//                    Métodos estáticos
	//-----------------------------------------------------

	/**
	 * Programa principal. Crea una instancia de esta clase, y la
	 * ejecuta simultáneamente en dos hebras diferentes. Espera a que
	 * ambas terminen y mira el valor sumado final, comprobando si es
	 * el esperado.
	 * 
	 * @param args Parámetros de la aplicación. Se ignoran.
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException {
		
		CondicionDeCarrera racer = new CondicionDeCarrera();
		Thread t1, t2;
		
		t1 = new Thread(racer);
		t2 = new Thread(racer);

		t1.start();
		t2.start();

		long resultadoEsperado;
		resultadoEsperado = NUMERO_SUMADO * NUM_VECES * 2;

		t1.join();
		t2.join();

		System.out.println("El resultado final es " + racer.getSuma());
		System.out.println("Esperábamos " + resultadoEsperado);

		if (racer.getSuma() != resultadoEsperado)
			System.out.println("¡¡¡NO COINCIDEN!!!");
		
	} // main

	//-----------------------------------------------------
	//                    Atributos privados
	//-----------------------------------------------------

	/**
	 * Atributo con el valor acumulado donde se realiza la suma.
	 * Hace las veces de variable compartida entre las dos hebras.
	 */
	private volatile long _suma = 0;
	
} // CondicionDeCarrera

Algunos comentarios del código:

Por comodidad, tenemos una única clase que hereda de Runnable yque, además, tiene el método main.

El main crea una única instancia de su clase, y la lanza dos veces, con dos Thread diferentes. De ese modo, las dos comparten el mismo objeto, que hace las veces de “recurso compartido”.

El único atributo del objeto es _suma. El método run() modifica ese atributo sumándole NUM_VECES veces el valor de NUMERO_SUMADO (ambas constantes).

Como hay dos hebras, y al principio _suma vale 0, al terminar ambas hebras _suma deberia valer 2NUMERO_SUMADONUM_VECES. En el programa principal esperamos a que las dos hebras terminen, y comprobamos si es así.

En el método run() las sumas se hacen con un for sumando de uno en uno.

Crea un proyecto nuevo en Eclipse, escribe el código y pruebalo. Ejecutalo también usando taskset o start /AFFINITY para comprobar los resultados con un único procesador. El programa deberia quejarse de que el valor esperado no es igual al obtenido, lo que indica una condición de carrera.

Si no lo consigues, haz más grande la constante NUMERO_SUMADO y/o añade Thread.yíeld() ; en la línea 14 (fuera del for).

Eres capaz de describir por que se produce el problema?